diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e430f06 --- /dev/null +++ b/__init__.py @@ -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"] diff --git a/index.json b/index.json new file mode 100644 index 0000000..59ebb56 --- /dev/null +++ b/index.json @@ -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" + } +] diff --git a/no-preview.png b/no-preview.png new file mode 100644 index 0000000..e2beb26 Binary files /dev/null and b/no-preview.png differ diff --git a/web/model-manager.css b/web/model-manager.css new file mode 100644 index 0000000..b181362 --- /dev/null +++ b/web/model-manager.css @@ -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; +} diff --git a/web/model-manager.js b/web/model-manager.js new file mode 100644 index 0000000..246d8a4 --- /dev/null +++ b/web/model-manager.js @@ -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} */ + #head = {}; + /** @type {Record} */ + #body = {}; + + /** + * @param {Array} 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} option + * @param {Array} tabs + */ +function $tabs(tabs) { + const instance = new Tabs(tabs); + return instance.element; +} + +/** + * @param {string} name + * @param {Array} 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} */ + #columns = []; + + /** @type {Array>} */ + #dataSource = []; + + /** @type {HTMLDivElement} */ + #tbody = null; + + /** + * @param {Array} 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 = "

No Models

"; + } + } +} + +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} */ + 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(); + }, + }); + }, +});