feat: base function
This commit is contained in:
174
__init__.py
Normal file
174
__init__.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
from aiohttp import web
|
||||||
|
import server
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
model_uri = os.path.join(os.getcwd(), "models")
|
||||||
|
extension_uri = os.path.join(os.getcwd(), "custom_nodes/ComfyUI-Model-Manager")
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@server.PromptServer.instance.routes.get("/model-manager/imgPreview")
|
||||||
|
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()
|
||||||
|
|
||||||
|
return web.Response(body=image_data, content_type="image/png")
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
@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:
|
||||||
|
dataSource = json.load(file)
|
||||||
|
else:
|
||||||
|
response = requests.get(uri)
|
||||||
|
dataSource = response.json()
|
||||||
|
|
||||||
|
# check if it installed
|
||||||
|
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
|
||||||
|
|
||||||
|
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_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)]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def download_model_file(url, filename):
|
||||||
|
dl_filename = filename + ".download"
|
||||||
|
|
||||||
|
rh = requests.get(
|
||||||
|
url=url, stream=True, verify=False, headers=def_headers, proxies=None
|
||||||
|
)
|
||||||
|
print("temp file is " + dl_filename)
|
||||||
|
total_size = int(rh.headers["Content-Length"])
|
||||||
|
|
||||||
|
basename, ext = os.path.splitext(filename)
|
||||||
|
print("Start download {}, file size: {}".format(basename, total_size))
|
||||||
|
|
||||||
|
downloaded_size = 0
|
||||||
|
if os.path.exists(dl_filename):
|
||||||
|
downloaded_size = os.path.getsize(download_file)
|
||||||
|
|
||||||
|
headers = {"Range": "bytes=%d-" % downloaded_size}
|
||||||
|
headers["User-Agent"] = def_headers["User-Agent"]
|
||||||
|
|
||||||
|
r = requests.get(url=url, stream=True, verify=False, headers=headers, proxies=None)
|
||||||
|
|
||||||
|
with open(dl_filename, "ab") as f:
|
||||||
|
for chunk in r.iter_content(chunk_size=1024):
|
||||||
|
if chunk:
|
||||||
|
downloaded_size += len(chunk)
|
||||||
|
f.write(chunk)
|
||||||
|
f.flush()
|
||||||
|
|
||||||
|
progress = int(50 * downloaded_size / total_size)
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stdout.write(
|
||||||
|
"\r[%s%s] %d%%"
|
||||||
|
% (
|
||||||
|
"-" * progress,
|
||||||
|
" " * (50 - progress),
|
||||||
|
100 * downloaded_size / total_size,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
print()
|
||||||
|
os.rename(dl_filename, filename)
|
||||||
|
|
||||||
|
|
||||||
|
@server.PromptServer.instance.routes.post("/model-manager/download")
|
||||||
|
async def download_file(request):
|
||||||
|
body = await request.json()
|
||||||
|
model_type = body.get("type")
|
||||||
|
model_type_path = model_type_dir_dict.get(model_type)
|
||||||
|
if model_type_path is None:
|
||||||
|
return web.json_response({"success": False})
|
||||||
|
|
||||||
|
download_uri = body.get("download")
|
||||||
|
if download_uri is None:
|
||||||
|
return web.json_response({"success": False})
|
||||||
|
|
||||||
|
model_name = body.get("name")
|
||||||
|
file_name = os.path.join(model_uri, model_type_path, model_name)
|
||||||
|
download_model_file(download_uri, file_name)
|
||||||
|
print("文件下载完成!")
|
||||||
|
return web.json_response({"success": True})
|
||||||
|
|
||||||
|
|
||||||
|
WEB_DIRECTORY = "web"
|
||||||
|
NODE_CLASS_MAPPINGS = {}
|
||||||
|
__all__ = ["NODE_CLASS_MAPPINGS"]
|
||||||
124
index.json
Normal file
124
index.json
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "sd-xl",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "sd-xl",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "vae",
|
||||||
|
"base": "sd-xl-vae",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "vae",
|
||||||
|
"name": "anything_v5.vae.safetensors",
|
||||||
|
"download": "https://huggingface.co/stablediffusionapi/anything-v5/resolve/main/vae/diffusion_pytorch_model.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "checkpoint",
|
||||||
|
"name": "Counterfeit-V3.0.safetensors",
|
||||||
|
"download": "https://huggingface.co/gsdf/Counterfeit-V3.0/resolve/main/Counterfeit-V3.0.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "embeddings",
|
||||||
|
"name": "EasyNegative.safetensors",
|
||||||
|
"download": "https://huggingface.co/datasets/gsdf/EasyNegative/resolve/main/EasyNegative.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "checkpoint",
|
||||||
|
"name": "CounterfeitXL_%CE%B2.safetensors",
|
||||||
|
"download": "https://huggingface.co/gsdf/CounterfeitXL/resolve/main/CounterfeitXL_%CE%B2.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "checkpoint",
|
||||||
|
"name": "AOM3A1B_orangemixs.safetensors",
|
||||||
|
"download": "https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1B_orangemixs.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "vae",
|
||||||
|
"name": "orangemix.vae.pt",
|
||||||
|
"download": "https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "checkpoint",
|
||||||
|
"name": "Deliberate.safetensors",
|
||||||
|
"download": "https://huggingface.co/XpucT/Deliberate/resolve/main/Deliberate.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "checkpoint",
|
||||||
|
"name": "LOFI_V3.safetensors",
|
||||||
|
"download": "https://huggingface.co/lenML/LOFI-v3/resolve/main/LOFI_V3.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "checkpoint",
|
||||||
|
"name": "ProtoGen_X5.8.safetensors",
|
||||||
|
"download": "https://huggingface.co/darkstorm2150/Protogen_x5.8_Official_Release/resolve/main/ProtoGen_X5.8.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "checkpoint",
|
||||||
|
"name": "GuoFeng3.4.safetensors",
|
||||||
|
"download": "https://huggingface.co/xiaolxl/GuoFeng3/resolve/main/GuoFeng3.4.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lora",
|
||||||
|
"name": "Xiaorenshu_v15.safetensors",
|
||||||
|
"download": "https://huggingface.co/datamonet/xiaorenshu/resolve/main/Xiaorenshu_v15.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lora",
|
||||||
|
"name": "Colorwater_v4.safetensors",
|
||||||
|
"download": "https://huggingface.co/niitokikei/Colorwater/resolve/main/Colorwater_v4.safetensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lora",
|
||||||
|
"name": "huyefo-v1.0.safetensors",
|
||||||
|
"download": "https://civitai.com/api/download/models/104426"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "upscale_models",
|
||||||
|
"name": "RealESRGAN_x2plus.pth",
|
||||||
|
"download": "https://huggingface.co/Rainy-hh/Real-ESRGAN/resolve/main/RealESRGAN_x2plus.pth"
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
no-preview.png
Normal file
BIN
no-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
205
web/model-manager.css
Normal file
205
web/model-manager.css
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
/* comfy table */
|
||||||
|
.comfy-table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-table .table-head tr {
|
||||||
|
background-color: var(--tr-even-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* comfy tabs */
|
||||||
|
.comfy-tabs {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-tabs-head {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border-bottom: 1px solid #6a6a6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-tabs-head .head-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #6a6a6a;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-tabs-head .head-item.active {
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
cursor: default;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-tabs-body {
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
border: 1px solid #6a6a6a;
|
||||||
|
border-top: none;
|
||||||
|
padding: 16px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* comfy grid */
|
||||||
|
.comfy-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-grid .item {
|
||||||
|
position: relative;
|
||||||
|
width: 230px;
|
||||||
|
height: 345px;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-grid .item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-grid .item p {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
background-color: #000a;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 9px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* comfy radio group */
|
||||||
|
.comfy-radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-radio {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: var(--input-text);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* model manager */
|
||||||
|
.model-manager {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: unset;
|
||||||
|
max-height: unset;
|
||||||
|
padding: 10px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager .comfy-modal-content {
|
||||||
|
width: 100%;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* model manager common */
|
||||||
|
.model-manager button,
|
||||||
|
.model-manager select,
|
||||||
|
.model-manager input {
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager button:disabled,
|
||||||
|
.model-manager select:disabled,
|
||||||
|
.model-manager input:disabled {
|
||||||
|
background-color: #6a6a6a;
|
||||||
|
filter: brightness(1.2);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager button.block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-table a {
|
||||||
|
color: #007acc;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager ::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager ::-webkit-scrollbar-track {
|
||||||
|
background-color: #353535;
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager ::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #a1a1a1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* model manager row */
|
||||||
|
.model-manager .row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* comfy tabs */
|
||||||
|
.model-manager .comfy-tabs {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager .comfy-tabs-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager .comfy-tabs-body > div {
|
||||||
|
position: relative;
|
||||||
|
max-height: 100%;
|
||||||
|
padding: 0 16px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* model manager special */
|
||||||
|
.model-manager .close {
|
||||||
|
position: absolute;
|
||||||
|
padding: 1px 6px;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager .row {
|
||||||
|
position: sticky;
|
||||||
|
padding-top: 2px;
|
||||||
|
margin-top: -2px;
|
||||||
|
padding-bottom: 18px;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
top: 0px;
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager .table-head {
|
||||||
|
position: sticky;
|
||||||
|
top: 52px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager div[data-name="Model List"] .row {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
536
web/model-manager.js
Normal file
536
web/model-manager.js
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
import { api } from "../../scripts/api.js";
|
||||||
|
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||||
|
|
||||||
|
function debounce(func, delay) {
|
||||||
|
let timer;
|
||||||
|
return function () {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
func.apply(this, arguments);
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tabs {
|
||||||
|
/** @type {Record<string, HTMLDivElement>} */
|
||||||
|
#head = {};
|
||||||
|
/** @type {Record<string, HTMLDivElement>} */
|
||||||
|
#body = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<HTMLDivElement>} tabs
|
||||||
|
*/
|
||||||
|
constructor(tabs) {
|
||||||
|
const head = [];
|
||||||
|
const body = [];
|
||||||
|
|
||||||
|
tabs.forEach((el, index) => {
|
||||||
|
const name = el.getAttribute("data-name");
|
||||||
|
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
const tag = $el(
|
||||||
|
"div.head-item",
|
||||||
|
{ onclick: () => this.active(name) },
|
||||||
|
[name]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
this.#active = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#head[name] = tag;
|
||||||
|
head.push(tag);
|
||||||
|
this.#body[name] = el;
|
||||||
|
body.push(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.element = $el("div.comfy-tabs", [
|
||||||
|
$el("div.comfy-tabs-head", head),
|
||||||
|
$el("div.comfy-tabs-body", body),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.active(this.#active);
|
||||||
|
}
|
||||||
|
|
||||||
|
#active = undefined;
|
||||||
|
|
||||||
|
active(name) {
|
||||||
|
this.#active = name;
|
||||||
|
Object.keys(this.#head).forEach((key) => {
|
||||||
|
if (name === key) {
|
||||||
|
this.#head[key].classList.add("active");
|
||||||
|
this.#body[key].style.display = "";
|
||||||
|
} else {
|
||||||
|
this.#head[key].classList.remove("active");
|
||||||
|
this.#body[key].style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, any>} option
|
||||||
|
* @param {Array<HTMLDivElement>} tabs
|
||||||
|
*/
|
||||||
|
function $tabs(tabs) {
|
||||||
|
const instance = new Tabs(tabs);
|
||||||
|
return instance.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Array<HTMLDivElement>} el
|
||||||
|
* @returns {HTMLDivElement}
|
||||||
|
*/
|
||||||
|
function $tab(name, el) {
|
||||||
|
return $el("div", { dataset: { name } }, el);
|
||||||
|
}
|
||||||
|
|
||||||
|
class List {
|
||||||
|
/**
|
||||||
|
* @typedef Column
|
||||||
|
* @prop {string} title
|
||||||
|
* @prop {string} dataIndex
|
||||||
|
* @prop {number} width
|
||||||
|
* @prop {string} align
|
||||||
|
* @prop {Function} render
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Array<Column>} */
|
||||||
|
#columns = [];
|
||||||
|
|
||||||
|
/** @type {Array<Record<string, any>>} */
|
||||||
|
#dataSource = [];
|
||||||
|
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
#tbody = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<Column>} columns
|
||||||
|
*/
|
||||||
|
constructor(columns) {
|
||||||
|
this.#columns = columns;
|
||||||
|
|
||||||
|
const colgroup = $el(
|
||||||
|
"colgroup",
|
||||||
|
columns.map((item) => {
|
||||||
|
return $el("col", {
|
||||||
|
style: { width: `${item.width}px` },
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const listTitle = $el(
|
||||||
|
"tr",
|
||||||
|
columns.map((item) => {
|
||||||
|
return $el("th", [item.title ?? ""]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.element = $el("table.comfy-table", [
|
||||||
|
colgroup.cloneNode(true),
|
||||||
|
$el("thead.table-head", [listTitle]),
|
||||||
|
$el("tbody.table-body", { $: (el) => (this.#tbody = el) }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(dataSource) {
|
||||||
|
this.#dataSource = dataSource;
|
||||||
|
this.#updateList();
|
||||||
|
}
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
return this.#dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateList() {
|
||||||
|
this.#tbody.innerHTML = null;
|
||||||
|
this.#tbody.append.apply(
|
||||||
|
this.#tbody,
|
||||||
|
this.#dataSource.map((row, index) => {
|
||||||
|
const cells = this.#columns.map((item) => {
|
||||||
|
const dataIndex = item.dataIndex;
|
||||||
|
const cellValue = row[dataIndex] ?? "";
|
||||||
|
const content = item.render
|
||||||
|
? item.render(cellValue, row, index)
|
||||||
|
: cellValue ?? "-";
|
||||||
|
|
||||||
|
const style = { textAlign: item.align };
|
||||||
|
return $el("td", { style }, [content]);
|
||||||
|
});
|
||||||
|
return $el("tr", cells);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Grid {
|
||||||
|
constructor() {
|
||||||
|
this.element = $el("div.comfy-grid");
|
||||||
|
}
|
||||||
|
|
||||||
|
#dataSource = [];
|
||||||
|
|
||||||
|
setData(dataSource) {
|
||||||
|
this.#dataSource = dataSource;
|
||||||
|
this.element.innerHTML = [];
|
||||||
|
this.#updateList();
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateList() {
|
||||||
|
this.element.innerHTML = null;
|
||||||
|
if (this.#dataSource.length > 0) {
|
||||||
|
this.element.append.apply(
|
||||||
|
this.element,
|
||||||
|
this.#dataSource.map((item) => {
|
||||||
|
const uri = item.post ?? "no-post";
|
||||||
|
const imgUrl = `/model-manager/imgPreview?uri=${uri}`;
|
||||||
|
return $el("div.item", {}, [
|
||||||
|
$el("img", { src: imgUrl }),
|
||||||
|
$el("p", [item.name]),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.element.innerHTML = "<h2>No Models</h2>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function $radioGroup(attr) {
|
||||||
|
const { name = Date.now(), onchange, options = [], $ } = attr;
|
||||||
|
|
||||||
|
/** @type {HTMLDivElement[]} */
|
||||||
|
const radioGroup = options.map((item, index) => {
|
||||||
|
const inputRef = { value: null };
|
||||||
|
|
||||||
|
return $el(
|
||||||
|
"div.comfy-radio",
|
||||||
|
{ onclick: () => inputRef.value.click() },
|
||||||
|
[
|
||||||
|
$el("input.radio-input", {
|
||||||
|
type: "radio",
|
||||||
|
name: name,
|
||||||
|
value: item.value,
|
||||||
|
checked: index === 0,
|
||||||
|
$: (el) => (inputRef.value = el),
|
||||||
|
}),
|
||||||
|
$el("label", [item.label ?? item.value]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const element = $el("input", { value: options[0]?.value });
|
||||||
|
$?.(element);
|
||||||
|
|
||||||
|
radioGroup.forEach((radio) => {
|
||||||
|
radio.addEventListener("change", (event) => {
|
||||||
|
const selectedValue = event.target.value;
|
||||||
|
element.value = selectedValue;
|
||||||
|
onchange?.(selectedValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return $el("div.comfy-radio-group", radioGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModelManager extends ComfyDialog {
|
||||||
|
#request(url, options) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api.fetchApi(url, options)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#el = {
|
||||||
|
loadSourceBtn: null,
|
||||||
|
loadSourceFromSelect: null,
|
||||||
|
loadSourceFromInput: null,
|
||||||
|
sourceInstalledFilter: null,
|
||||||
|
sourceContentFilter: null,
|
||||||
|
sourceFilterBtn: null,
|
||||||
|
modelTypeSelect: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
#data = {
|
||||||
|
sourceList: [],
|
||||||
|
models: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {List} */
|
||||||
|
#sourceList = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.element = $el(
|
||||||
|
"div.comfy-modal.model-manager",
|
||||||
|
{ parent: document.body },
|
||||||
|
[
|
||||||
|
$el("div.comfy-modal-content", [
|
||||||
|
$el("button.close", {
|
||||||
|
textContent: "X",
|
||||||
|
onclick: () => this.close(),
|
||||||
|
}),
|
||||||
|
$tabs([
|
||||||
|
$tab("Source Install", this.#createSourceInstall()),
|
||||||
|
$tab("Customer Install", []),
|
||||||
|
$tab("Model List", this.#createModelList()),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#init();
|
||||||
|
}
|
||||||
|
|
||||||
|
#init() {
|
||||||
|
this.#refreshSourceList();
|
||||||
|
this.#refreshModelList();
|
||||||
|
}
|
||||||
|
|
||||||
|
#createSourceInstall() {
|
||||||
|
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("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(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
this.#sourceList.element,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#createSourceList() {
|
||||||
|
const sourceList = new List([
|
||||||
|
{
|
||||||
|
title: "Type",
|
||||||
|
dataIndex: "type",
|
||||||
|
width: "120",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Base",
|
||||||
|
dataIndex: "base",
|
||||||
|
width: "120",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
dataIndex: "name",
|
||||||
|
width: "280",
|
||||||
|
render: (value, record) => {
|
||||||
|
const href = record.page;
|
||||||
|
return $el("a", { target: "_blank", href }, [value]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Description",
|
||||||
|
dataIndex: "description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Download",
|
||||||
|
width: "150",
|
||||||
|
render: (_, record) => {
|
||||||
|
const installed = record.installed;
|
||||||
|
return $el("button.block", {
|
||||||
|
type: "button",
|
||||||
|
disabled: installed,
|
||||||
|
textContent: installed ? "Installed" : "Install",
|
||||||
|
onclick: async (e) => {
|
||||||
|
e.disabled = true;
|
||||||
|
const response = await this.#request(
|
||||||
|
"/model-manager/download",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(record),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(response);
|
||||||
|
e.disabled = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
this.#sourceList = sourceList;
|
||||||
|
return sourceList.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 dataSource = await this.#request(
|
||||||
|
`/model-manager/source?uri=${uri}`
|
||||||
|
).catch(() => []);
|
||||||
|
this.#data.sourceList = 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#sourceList.setData(newDataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Grid} */
|
||||||
|
#modelList = null;
|
||||||
|
|
||||||
|
#createModelList() {
|
||||||
|
const gridInstance = new Grid();
|
||||||
|
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(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
gridInstance.element,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async #refreshModelList() {
|
||||||
|
const dataSource = await this.#request("/model-manager/models");
|
||||||
|
this.#data.models = dataSource;
|
||||||
|
this.#updateModelList();
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateModelList() {
|
||||||
|
const type = this.#el.modelTypeSelect.value;
|
||||||
|
const list = this.#data.models[type];
|
||||||
|
this.#modelList.setData(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {ModelManager}
|
||||||
|
*/
|
||||||
|
function getInstance() {
|
||||||
|
if (!instance) {
|
||||||
|
instance = new ModelManager();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "Comfy.ModelManager",
|
||||||
|
|
||||||
|
async setup() {
|
||||||
|
$el("link", {
|
||||||
|
parent: document.head,
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: "./extensions/ComfyUI-Model-Manager/model-manager.css",
|
||||||
|
});
|
||||||
|
|
||||||
|
$el("button", {
|
||||||
|
parent: document.querySelector(".comfy-menu"),
|
||||||
|
textContent: "Models",
|
||||||
|
style: { order: 1 },
|
||||||
|
onclick: () => {
|
||||||
|
getInstance().show();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user