mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2025-12-16 18:02:58 +08:00
merged updates from upstream, and following output types work: SaveImage, PreviewImage, AnimateDiffCombine, VideoCombine
This commit is contained in:
commit
44f3e2b0b4
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@ __pycache__/
|
||||
.vscode/
|
||||
.tmp
|
||||
config.ini
|
||||
snapshots/**
|
||||
startup-scripts/**
|
||||
|
||||
26
README.md
26
README.md
@ -97,6 +97,18 @@ This repository provides Colab notebooks that allow you to install and use Comfy
|
||||
|
||||

|
||||
|
||||
|
||||
## Snapshot-Manager
|
||||
* When you press `Save snapshot` or use `Update All` on `Manager Menu`, the current installation status snapshot is saved.
|
||||
* Snapshot file dir: `ComfyUI-Manager/snapshots`
|
||||
* You can rename snapshot file.
|
||||
* Press the "Restore" button to revert to the installation status of the respective snapshot.
|
||||
* However, for custom nodes not managed by Git, snapshot support is incomplete.
|
||||
* When you press `Restore`, it will take effect on the next ComfyUI startup.
|
||||
|
||||
|
||||

|
||||
|
||||
## How to register your custom node into ComfyUI-Manager
|
||||
|
||||
* Add an entry to `custom-node-list.json` located in the root of ComfyUI-Manager and submit a Pull Request.
|
||||
@ -157,10 +169,6 @@ NODE_CLASS_MAPPINGS.update({
|
||||
|
||||

|
||||
|
||||
* Currently, support is not available for custom nodes that can only be downloaded through civitai.
|
||||
|
||||
* [ComfyUI_Custom_Nodes_AlekPet](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet)
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
* If your `git.exe` is installed in a specific location other than system git, please install ComfyUI-Manager and run ComfyUI. Then, specify the path including the file name in `git_exe = ` in the ComfyUI-Manager/config.ini file that is generated.
|
||||
@ -182,12 +190,12 @@ NODE_CLASS_MAPPINGS.update({
|
||||
- [x] category/keyword filter
|
||||
- [x] Automatic recognition of missing custom nodes
|
||||
- [x] Automatic installation suggestion of missing custom nodes
|
||||
- [x] 3rd party repository
|
||||
- [x] 3rd party repository -> channels
|
||||
- [x] Specification of custom nodes
|
||||
- [x] Specification scanner
|
||||
- [x] workflow download -> workflow gallery
|
||||
- [x] Search extension by node name -> [ltdrdata.github.io](https://ltdrdata.github.io)
|
||||
- [ ] installation from git url
|
||||
- [ ] Specification of custom nodes
|
||||
- [ ] Specification scanner
|
||||
- [ ] Search extension by node name
|
||||
- [ ] workflow downloader
|
||||
|
||||
|
||||
# Disclaimer
|
||||
|
||||
228
__init__.py
228
__init__.py
@ -5,12 +5,31 @@ import folder_paths
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import subprocess
|
||||
import datetime
|
||||
import re
|
||||
import locale
|
||||
import subprocess # don't remove this
|
||||
from tqdm.auto import tqdm
|
||||
import concurrent
|
||||
import ssl
|
||||
|
||||
version = "V0.41"
|
||||
print(f"### Loading: ComfyUI-Manager ({version})")
|
||||
|
||||
|
||||
def handle_stream(stream, prefix):
|
||||
for line in stream:
|
||||
print(prefix, line, end="")
|
||||
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
|
||||
for msg in stream:
|
||||
if prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg):
|
||||
if msg.startswith('100%'):
|
||||
print('\r' + msg, end="", file=sys.stderr),
|
||||
else:
|
||||
print('\r' + msg[:-1], end="", file=sys.stderr),
|
||||
else:
|
||||
if prefix == '[!]':
|
||||
print(prefix, msg, end="", file=sys.stderr)
|
||||
else:
|
||||
print(prefix, msg, end="")
|
||||
|
||||
|
||||
def run_script(cmd, cwd='.'):
|
||||
@ -52,13 +71,12 @@ except:
|
||||
print(f"## ComfyUI-Manager: installing dependencies done.")
|
||||
|
||||
|
||||
from git.remote import RemoteProgress
|
||||
|
||||
sys.path.append('../..')
|
||||
|
||||
from torchvision.datasets.utils import download_url
|
||||
|
||||
# ensure .js
|
||||
print("### Loading: ComfyUI-Manager (V0.36)")
|
||||
|
||||
comfy_ui_required_revision = 1240
|
||||
comfy_ui_revision = "Unknown"
|
||||
|
||||
@ -95,7 +113,8 @@ def write_config():
|
||||
'badge_mode': get_config()['badge_mode'],
|
||||
'git_exe': get_config()['git_exe'],
|
||||
'channel_url': get_config()['channel_url'],
|
||||
'channel_url_list': get_config()['channel_url_list']
|
||||
'channel_url_list': get_config()['channel_url_list'],
|
||||
'bypass_ssl': get_config()['bypass_ssl']
|
||||
}
|
||||
with open(config_path, 'w') as configfile:
|
||||
config.write(configfile)
|
||||
@ -125,7 +144,8 @@ def read_config():
|
||||
'badge_mode': default_conf['badge_mode'] if 'badge_mode' in default_conf else 'none',
|
||||
'git_exe': default_conf['git_exe'] if 'git_exe' in default_conf else '',
|
||||
'channel_url': default_conf['channel_url'] if 'channel_url' in default_conf else 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main',
|
||||
'channel_url_list': ch_url_list
|
||||
'channel_url_list': ch_url_list,
|
||||
'bypass_ssl': default_conf['bypass_ssl'] if 'bypass_ssl' in default_conf else False,
|
||||
}
|
||||
|
||||
except Exception:
|
||||
@ -134,7 +154,8 @@ def read_config():
|
||||
'badge_mode': 'none',
|
||||
'git_exe': '',
|
||||
'channel_url': 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main',
|
||||
'channel_url_list': ''
|
||||
'channel_url_list': '',
|
||||
'bypass_ssl': False
|
||||
}
|
||||
|
||||
|
||||
@ -284,6 +305,14 @@ def __win_check_git_pull(path):
|
||||
process.wait()
|
||||
|
||||
|
||||
def switch_to_default_branch(repo):
|
||||
show_result = repo.git.remote("show", "origin")
|
||||
matches = re.search(r"\s*HEAD branch:\s*(.*)", show_result)
|
||||
if matches:
|
||||
default_branch = matches.group(1)
|
||||
repo.git.checkout(default_branch)
|
||||
|
||||
|
||||
def git_repo_has_updates(path, do_fetch=False, do_update=False):
|
||||
if do_fetch:
|
||||
print(f"\x1b[2K\rFetching: {path}", end='')
|
||||
@ -300,9 +329,6 @@ def git_repo_has_updates(path, do_fetch=False, do_update=False):
|
||||
# Fetch the latest commits from the remote repository
|
||||
repo = git.Repo(path)
|
||||
|
||||
current_branch = repo.active_branch
|
||||
branch_name = current_branch.name
|
||||
|
||||
remote_name = 'origin'
|
||||
remote = repo.remote(name=remote_name)
|
||||
|
||||
@ -313,8 +339,11 @@ def git_repo_has_updates(path, do_fetch=False, do_update=False):
|
||||
remote.fetch()
|
||||
|
||||
if do_update:
|
||||
if repo.head.is_detached:
|
||||
switch_to_default_branch(repo)
|
||||
|
||||
try:
|
||||
remote.pull(rebase=True)
|
||||
remote.pull()
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
new_commit_hash = repo.head.commit.hexsha
|
||||
|
||||
@ -327,7 +356,13 @@ def git_repo_has_updates(path, do_fetch=False, do_update=False):
|
||||
except Exception as e:
|
||||
print(f"\nUpdating failed: {path}\n{e}", file=sys.stderr)
|
||||
|
||||
if repo.head.is_detached:
|
||||
return True
|
||||
|
||||
# Get commit hash of the remote branch
|
||||
current_branch = repo.active_branch
|
||||
branch_name = current_branch.name
|
||||
|
||||
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
|
||||
|
||||
# Compare the commit hashes to determine if the local repository is behind the remote repository
|
||||
@ -353,11 +388,17 @@ def git_pull(path):
|
||||
return __win_check_git_pull(path)
|
||||
else:
|
||||
repo = git.Repo(path)
|
||||
|
||||
print(f"path={path} / repo.is_dirty: {repo.is_dirty()}")
|
||||
|
||||
if repo.is_dirty():
|
||||
repo.git.stash()
|
||||
|
||||
if repo.head.is_detached:
|
||||
switch_to_default_branch(repo)
|
||||
|
||||
origin = repo.remote(name='origin')
|
||||
origin.pull(rebase=True)
|
||||
origin.pull()
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
|
||||
repo.close()
|
||||
@ -518,9 +559,13 @@ def check_custom_nodes_installed(json_obj, do_fetch=False, do_update_check=True,
|
||||
elif do_update_check:
|
||||
print("Start update check...", end="")
|
||||
|
||||
for item in json_obj['custom_nodes']:
|
||||
def process_custom_node(item):
|
||||
check_a_custom_node_installed(item, do_fetch, do_update_check, do_update)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(4) as executor:
|
||||
for item in json_obj['custom_nodes']:
|
||||
executor.submit(process_custom_node, item)
|
||||
|
||||
if do_fetch:
|
||||
print(f"\x1b[2K\rFetching done.")
|
||||
elif do_update:
|
||||
@ -569,6 +614,8 @@ async def fetch_updates(request):
|
||||
@server.PromptServer.instance.routes.get("/customnode/update_all")
|
||||
async def update_all(request):
|
||||
try:
|
||||
save_snapshot_with_postfix('autosave')
|
||||
|
||||
if request.rel_url.query["mode"] == "local":
|
||||
uri = local_db_custom_node_list
|
||||
else:
|
||||
@ -638,10 +685,9 @@ async def fetch_alternatives_list(request):
|
||||
|
||||
|
||||
def check_model_installed(json_obj):
|
||||
for item in json_obj['models']:
|
||||
item['installed'] = 'None'
|
||||
|
||||
def process_model(item):
|
||||
model_path = get_model_path(item)
|
||||
item['installed'] = 'None'
|
||||
|
||||
if model_path is not None:
|
||||
if os.path.exists(model_path):
|
||||
@ -649,6 +695,10 @@ def check_model_installed(json_obj):
|
||||
else:
|
||||
item['installed'] = 'False'
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(8) as executor:
|
||||
for item in json_obj['models']:
|
||||
executor.submit(process_model, item)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/externalmodel/getlist")
|
||||
async def fetch_externalmodel_list(request):
|
||||
@ -663,6 +713,125 @@ async def fetch_externalmodel_list(request):
|
||||
return web.json_response(json_obj, content_type='application/json')
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/snapshot/getlist")
|
||||
async def get_snapshot_list(request):
|
||||
snapshots_directory = os.path.join(os.path.dirname(__file__), 'snapshots')
|
||||
items = [f[:-5] for f in os.listdir(snapshots_directory) if f.endswith('.json')]
|
||||
items.sort(reverse=True)
|
||||
return web.json_response({'items': items}, content_type='application/json')
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/snapshot/remove")
|
||||
async def remove_snapshot(request):
|
||||
try:
|
||||
target = request.rel_url.query["target"]
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}.json")
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
return web.Response(status=200)
|
||||
except:
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/snapshot/restore")
|
||||
async def remove_snapshot(request):
|
||||
try:
|
||||
target = request.rel_url.query["target"]
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}.json")
|
||||
if os.path.exists(path):
|
||||
if not os.path.exists(startup_script_path):
|
||||
os.makedirs(startup_script_path)
|
||||
|
||||
target_path = os.path.join(startup_script_path, "restore-snapshot.json")
|
||||
shutil.copy(path, target_path)
|
||||
|
||||
print(f"Snapshot restore scheduled: `{target}`")
|
||||
return web.Response(status=200)
|
||||
|
||||
print(f"Snapshot file not found: `{path}`")
|
||||
return web.Response(status=400)
|
||||
except:
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
def get_current_snapshot():
|
||||
# Get ComfyUI hash
|
||||
repo_path = os.path.dirname(folder_paths.__file__)
|
||||
|
||||
if not os.path.exists(os.path.join(repo_path, '.git')):
|
||||
print(f"ComfyUI update fail: The installed ComfyUI does not have a Git repository.")
|
||||
return web.Response(status=400)
|
||||
|
||||
repo = git.Repo(repo_path)
|
||||
comfyui_commit_hash = repo.head.commit.hexsha
|
||||
|
||||
git_custom_nodes = {}
|
||||
file_custom_nodes = []
|
||||
|
||||
# Get custom nodes hash
|
||||
for path in os.listdir(custom_nodes_path):
|
||||
fullpath = os.path.join(custom_nodes_path, path)
|
||||
|
||||
if os.path.isdir(fullpath):
|
||||
is_disabled = path.endswith(".disabled")
|
||||
|
||||
try:
|
||||
git_dir = os.path.join(fullpath, '.git')
|
||||
|
||||
if not os.path.exists(git_dir):
|
||||
continue
|
||||
|
||||
repo = git.Repo(fullpath)
|
||||
commit_hash = repo.head.commit.hexsha
|
||||
url = repo.remotes.origin.url
|
||||
git_custom_nodes[url] = {
|
||||
'hash': commit_hash,
|
||||
'disabled': is_disabled
|
||||
}
|
||||
|
||||
except:
|
||||
print(f"Failed to extract snapshots for the custom node '{path}'.")
|
||||
|
||||
elif path.endswith('.py'):
|
||||
is_disabled = path.endswith(".py.disabled")
|
||||
filename = os.path.basename(path)
|
||||
item = {
|
||||
'filename': filename,
|
||||
'disabled': is_disabled
|
||||
}
|
||||
|
||||
file_custom_nodes.append(item)
|
||||
|
||||
return {
|
||||
'comfyui': comfyui_commit_hash,
|
||||
'git_custom_nodes': git_custom_nodes,
|
||||
'file_custom_nodes': file_custom_nodes,
|
||||
}
|
||||
|
||||
|
||||
def save_snapshot_with_postfix(postfix):
|
||||
now = datetime.datetime.now()
|
||||
|
||||
date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S")
|
||||
file_name = f"{date_time_format}_{postfix}"
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{file_name}.json")
|
||||
with open(path, "w") as json_file:
|
||||
json.dump(get_current_snapshot(), json_file, indent=4)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/snapshot/save")
|
||||
async def save_snapshot(request):
|
||||
try:
|
||||
save_snapshot_with_postfix('snapshot')
|
||||
return web.Response(status=200)
|
||||
except:
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
def unzip_install(files):
|
||||
temp_filename = 'manager-temp.zip'
|
||||
for url in files:
|
||||
@ -810,6 +979,18 @@ def execute_install_script(url, repo_path):
|
||||
return True
|
||||
|
||||
|
||||
class GitProgress(RemoteProgress):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pbar = tqdm()
|
||||
|
||||
def update(self, op_code, cur_count, max_count=None, message=''):
|
||||
self.pbar.total = max_count
|
||||
self.pbar.n = cur_count
|
||||
self.pbar.pos = 0
|
||||
self.pbar.refresh()
|
||||
|
||||
|
||||
def gitclone_install(files):
|
||||
print(f"install: {files}")
|
||||
for url in files:
|
||||
@ -824,7 +1005,7 @@ def gitclone_install(files):
|
||||
if platform.system() == 'Windows':
|
||||
run_script([sys.executable, git_script_path, "--clone", custom_nodes_path, url])
|
||||
else:
|
||||
repo = git.Repo.clone_from(url, repo_path, recursive=True)
|
||||
repo = git.Repo.clone_from(url, repo_path, recursive=True, progress=GitProgress())
|
||||
repo.git.clear_cache()
|
||||
repo.close()
|
||||
|
||||
@ -1073,6 +1254,9 @@ async def update_comfyui(request):
|
||||
# version check
|
||||
repo = git.Repo(repo_path)
|
||||
|
||||
if repo.head.is_detached:
|
||||
switch_to_default_branch(repo)
|
||||
|
||||
current_branch = repo.active_branch
|
||||
branch_name = current_branch.name
|
||||
|
||||
@ -1133,6 +1317,7 @@ async def install_model(request):
|
||||
if json_data['url'].startswith('https://github.com') or json_data['url'].startswith('https://huggingface.co'):
|
||||
model_dir = get_model_dir(json_data)
|
||||
download_url(json_data['url'], model_dir)
|
||||
|
||||
return web.json_response({}, content_type='application/json')
|
||||
else:
|
||||
res = download_url_with_agent(json_data['url'], model_path)
|
||||
@ -1419,6 +1604,11 @@ async def share_art(request):
|
||||
}
|
||||
}, content_type='application/json', status=200)
|
||||
|
||||
if get_config()['bypass_ssl']:
|
||||
ssl._create_default_https_context = ssl._create_unverified_context # SSL certificate error fix.
|
||||
|
||||
|
||||
WEB_DIRECTORY = "js"
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
__all__ = ['NODE_CLASS_MAPPINGS']
|
||||
|
||||
|
||||
@ -80,6 +80,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A collection of nodes which can be useful for animation in ComfyUI. The main focus of this extension is implementing a mechanism called loopchain. A loopchain in this case is the chain of nodes only executed repeatly in the workflow. If a node chain contains a loop node from this extension, it will become a loop chain."
|
||||
},
|
||||
{
|
||||
"author": "Fannovel16",
|
||||
"title": "ComfyUI MotionDiff",
|
||||
"reference": "https://github.com/Fannovel16/ComfyUI-MotionDiff",
|
||||
"files": [
|
||||
"https://github.com/Fannovel16/ComfyUI-MotionDiff"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Implementation of MDM, MotionDiffuse and ReMoDiffuse into ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "biegert",
|
||||
"title": "CLIPSeg",
|
||||
@ -141,14 +151,24 @@
|
||||
"description": "It provides the capability to generate CLIP from an image input, unlike unCLIP, which works in all models. (To use this extension, you need to download the required model file from <B>Install Models</B>)"
|
||||
},
|
||||
{
|
||||
"author": "LucianoCirino",
|
||||
"title": "Efficiency Nodes for ComfyUI",
|
||||
"reference": "https://github.com/LucianoCirino/efficiency-nodes-comfyui",
|
||||
"author": "jags111",
|
||||
"title": "Efficiency Nodes for ComfyUI Version 2.0+",
|
||||
"reference": "https://github.com/jags111/efficiency-nodes-comfyui",
|
||||
"files": [
|
||||
"https://github.com/LucianoCirino/efficiency-nodes-comfyui"
|
||||
"https://github.com/jags111/efficiency-nodes-comfyui"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A collection of ComfyUI custom nodes to help streamline workflows and reduce total node count."
|
||||
"description": "A collection of ComfyUI custom nodes to help streamline workflows and reduce total node count.<p style='background-color: black; color: red;'>NOTE: This node is originally created by LucianoCirino, but the <a href='https://github.com/LucianoCirino/efficiency-nodes-comfyui'>original repository</a> is no longer maintained and has been forked by a new maintainer. To use the forked version, you should uninstall the original version and <B>REINSTALL</B> this one.</p>"
|
||||
},
|
||||
{
|
||||
"author": "jags111",
|
||||
"title": "ComfyUI_Jags_VectorMagic",
|
||||
"reference": "https://github.com/jags111/ComfyUI_Jags_VectorMagic",
|
||||
"files": [
|
||||
"https://github.com/jags111/ComfyUI_Jags_VectorMagic"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "a collection of nodes to explore Vector and image manipulation"
|
||||
},
|
||||
{
|
||||
"author": "Derfuu",
|
||||
@ -447,6 +467,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension currently has two sets of nodes - one set for editing the contrast/color of images and another set for saving images as 16 bit PNG files."
|
||||
},
|
||||
{
|
||||
"author": "city96",
|
||||
"title": "Extra Models for ComfyUI",
|
||||
"reference": "https://github.com/city96/ComfyUI_ExtraModels",
|
||||
"files": [
|
||||
"https://github.com/city96/ComfyUI_ExtraModels"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension aims to add support for various random image diffusion models to ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "Kaharos94",
|
||||
"title": "ComfyUI-Saveaswebp",
|
||||
@ -753,13 +783,13 @@
|
||||
},
|
||||
{
|
||||
"author": "bmad4ever",
|
||||
"title": "ComfyUI-Bmad-Custom-Nodes",
|
||||
"reference": "https://github.com/bmad4ever/ComfyUI-Bmad-Custom-Nodes",
|
||||
"title": "Bmad Nodes",
|
||||
"reference": "https://github.com/bmad4ever/comfyui_bmad_nodes",
|
||||
"files": [
|
||||
"https://github.com/bmad4ever/ComfyUI-Bmad-Custom-Nodes"
|
||||
"https://github.com/bmad4ever/comfyui_bmad_nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This custom node offers the following functionalities: API support for setting up API requests, computer vision primarily for masking purposes using GrabCut or contours, and general utility to streamline workflow setup or implement essential missing features."
|
||||
"description": "This custom node offers the following functionalities: API support for setting up API requests, computer vision primarily for masking or collages, and general utility to streamline workflow setup or implement essential missing features."
|
||||
},
|
||||
{
|
||||
"author": "FizzleDorf",
|
||||
@ -1156,7 +1186,7 @@
|
||||
},
|
||||
{
|
||||
"author": "Gourieff",
|
||||
"title": "ReActor Node 0.1.0 for ComfyUI",
|
||||
"title": "ReActor Node for ComfyUI",
|
||||
"reference": "https://github.com/Gourieff/comfyui-reactor-node",
|
||||
"files": [
|
||||
"https://github.com/Gourieff/comfyui-reactor-node"
|
||||
@ -1294,6 +1324,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: KepRotateImage"
|
||||
},
|
||||
{
|
||||
"author": "M1kep",
|
||||
"title": "ComfyUI-OtherVAEs",
|
||||
"reference": "https://github.com/M1kep/ComfyUI-OtherVAEs",
|
||||
"files": [
|
||||
"https://github.com/M1kep/ComfyUI-OtherVAEs"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: TAESD VAE Decode"
|
||||
},
|
||||
{
|
||||
"author": "uarefans",
|
||||
"title": "ComfyUI-Fans",
|
||||
@ -1421,6 +1461,7 @@
|
||||
"files": [
|
||||
"https://github.com/rgthree/rgthree-comfy"
|
||||
],
|
||||
"nodename_pattern": " (rgthree)$",
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Seed, Reroute, Context, Lora Loader Stack, Context Switch, Fast Muter. These custom nodes helps organize the building of complex workflows."
|
||||
},
|
||||
@ -1565,14 +1606,14 @@
|
||||
"description": "Nodes:TC_EqualizeCLAHE, TC_SizeApproximation, TC_ImageResize, TC_ImageScale, TC_ColorFill."
|
||||
},
|
||||
{
|
||||
"author": "TeaCrab",
|
||||
"title": "ComfyUI-TeaNodes",
|
||||
"author": "nagolinc",
|
||||
"title": "ComfyUI_FastVAEDecorder_SDXL",
|
||||
"reference": "https://github.com/nagolinc/ComfyUI_FastVAEDecorder_SDXL",
|
||||
"files": [
|
||||
"https://github.com/nagolinc/ComfyUI_FastVAEDecorder_SDXL"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:FastLatentToImage"
|
||||
"description": "Based off of: <a href='https://github.com/Birch-san/diffusers-play/tree/main/approx_vae'>Birch-san/diffusers-play/approx_vae</a>. This ComfyUI node allows you to quickly preview SDXL 1.0 latents."
|
||||
},
|
||||
{
|
||||
"author": "bradsec",
|
||||
@ -1724,16 +1765,6 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A custom ComfyUI node designed for rapid latent upscaling using a compact neural network, eliminating the need for VAE-based decoding and encoding."
|
||||
},
|
||||
{
|
||||
"author": "GeLi1989",
|
||||
"title": "roop nodes for ComfyUI",
|
||||
"reference": "https://github.com/GeLi1989/GK-beifen-ComfyUI_roop",
|
||||
"files": [
|
||||
"https://github.com/GeLi1989/GK-beifen-ComfyUI_roop"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI nodes for the roop A1111 webui script. NOTE: Need to download <a href='https://github.com/SumDeusVitae/FaceSwap_v01/raw/main/inswapper_128.onnx' target='blank'/>model</a> to use this node."
|
||||
},
|
||||
{
|
||||
"author": "spro",
|
||||
"title": "Latent Mirror node for ComfyUI",
|
||||
@ -2013,6 +2044,16 @@
|
||||
"nodename_pattern": "^(UE\\? |UE )",
|
||||
"description": "A set of nodes that allow data to be 'broadcast' to some or all unconnected inputs. Greatly reduces link spaghetti."
|
||||
},
|
||||
{
|
||||
"author": "chrisgoringe",
|
||||
"title": "Prompt Info",
|
||||
"reference": "https://github.com/chrisgoringe/cg-prompt-info",
|
||||
"files": [
|
||||
"https://github.com/chrisgoringe/cg-prompt-info"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Prompt Info"
|
||||
},
|
||||
{
|
||||
"author": "TGu-97",
|
||||
"title": "TGu Utilities",
|
||||
@ -2070,7 +2111,6 @@
|
||||
"files": [
|
||||
"https://github.com/kijai/ComfyUI-KJNodes"
|
||||
],
|
||||
"pip": ["librosa"],
|
||||
"install_type": "git-clone",
|
||||
"description": "Various quality of life -nodes for ComfyUI, mostly just visual stuff to improve usability."
|
||||
},
|
||||
@ -2152,7 +2192,7 @@
|
||||
"https://github.com/a1lazydog/ComfyUI-AudioScheduler"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: LoadAudioFromPath, AudioToFFTs, BatchAmplitudeSchedule, AmplitudeSchedule."
|
||||
"description": "Load mp3 files and use the audio nodes to power animations and prompt scheduling. Use with FizzNodes."
|
||||
},
|
||||
{
|
||||
"author": "whatbirdisthat",
|
||||
@ -2166,13 +2206,13 @@
|
||||
},
|
||||
{
|
||||
"author": "chrish-slingshot",
|
||||
"title": "ComfyUI-ImageGlitcher",
|
||||
"reference": "https://github.com/chrish-slingshot/ComfyUI-ImageGlitcher",
|
||||
"title": "CrasH Utils",
|
||||
"reference": "https://github.com/chrish-slingshot/CrasHUtils",
|
||||
"files": [
|
||||
"https://github.com/chrish-slingshot/ComfyUI-ImageGlitcher"
|
||||
"https://github.com/chrish-slingshot/CrasHUtils"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: ImageGlitcher. Based on the HTML image glitcher by Felix Turner <a href='https://www.airtightinteractive.com/demos/js/imageglitcher/' target='blank'>here</a>."
|
||||
"description": "A mixture of effects and quality of life nodes. Nodes: ImageGlitcher (gives an image a cool glitchy effect), ColorStylizer (highlights a single color in an image), QueryLocalLLM (queries a local LLM API though oobabooga), SDXLReslution (resolution picker for the standard SDXL resolutions, the complete list), SDXLResolutionSplit (splits the SDXL resolution into width and height). "
|
||||
},
|
||||
{
|
||||
"author": "spinagon",
|
||||
@ -2202,7 +2242,7 @@
|
||||
"https://github.com/chibiace/ComfyUI-Chibi-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Loader, Prompts, ..."
|
||||
"description": "Nodes:Loader, Prompts, ImageTool, Wildcards, LoadEmbedding, ConditionText, SaveImages, ..."
|
||||
},
|
||||
{
|
||||
"author": "DigitalIO",
|
||||
@ -2224,6 +2264,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:RetainFace, FaceFusion, RatioMerge2Image, MaskMerge2Image, ReplaceBoxImg, ExpandMaskBox, FaceSkin, SkinRetouching, PortraitEnhancement, ..."
|
||||
},
|
||||
{
|
||||
"author": "THtianhao",
|
||||
"title": "ComfyUI-FaceChain",
|
||||
"reference": "https://github.com/THtianhao/ComfyUI-FaceChain",
|
||||
"files": [
|
||||
"https://github.com/THtianhao/ComfyUI-FaceChain"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:FC_LoraMerge."
|
||||
},
|
||||
{
|
||||
"author": "zer0TF",
|
||||
"title": "Cute Comfy",
|
||||
@ -2244,6 +2294,57 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A text-to-speech plugin used under ComfyUI. It utilizes the Microsoft Speech TTS interface to convert text content into MP3 format audio files."
|
||||
},
|
||||
{
|
||||
"author": "drustan-hawk",
|
||||
"title": "primitive-types",
|
||||
"reference": "https://github.com/drustan-hawk/primitive-types",
|
||||
"files": [
|
||||
"https://github.com/drustan-hawk/primitive-types"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A text-to-speech plugin used under ComfyUI. It utilizes the Microsoft Speech TTS interface to convert text content into MP3 format audio files."
|
||||
},
|
||||
{
|
||||
"author": "shadowcz007",
|
||||
"title": "comfyui-mixlab-nodes [WIP]",
|
||||
"reference": "https://github.com/shadowcz007/comfyui-mixlab-nodes",
|
||||
"files": [
|
||||
"https://github.com/shadowcz007/comfyui-mixlab-nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: RandomPrompt, TransparentImage, LoadImageFromPath, SplitLongMask, ImagesCrop, RunWorkflow"
|
||||
},
|
||||
{
|
||||
"author": "ostris",
|
||||
"title": "Ostris Nodes ComfyUI",
|
||||
"reference": "https://github.com/ostris/ostris_nodes_comfyui",
|
||||
"files": [
|
||||
"https://github.com/ostris/ostris_nodes_comfyui"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"nodename_pattern": "- Ostris$",
|
||||
"description": "This is a collection of custom nodes for ComfyUI that I made for some QOL. I will be adding much more advanced ones in the future once I get more familiar with the API."
|
||||
},
|
||||
{
|
||||
"author": "0xbitches",
|
||||
"title": "Latent Consistency Model for ComfyUI",
|
||||
"reference": "https://github.com/0xbitches/ComfyUI-LCM",
|
||||
"files": [
|
||||
"https://github.com/0xbitches/ComfyUI-LCM"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This custom node implements a Latent Consistency Model sampler in ComfyUI. (LCM)"
|
||||
},
|
||||
{
|
||||
"author": "aszc-dev",
|
||||
"title": "Core ML Suite for ComfyUI",
|
||||
"reference": "https://github.com/aszc-dev/ComfyUI-CoreMLSuite",
|
||||
"files": [
|
||||
"https://github.com/aszc-dev/ComfyUI-CoreMLSuite"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension contains a set of custom nodes for ComfyUI that allow you to use Core ML models in your ComfyUI workflows. The models can be obtained here, or you can convert your own models using coremltools. The main motivation behind using Core ML models in ComfyUI is to allow you to utilize the ANE (Apple Neural Engine) on Apple Silicon (M1/M2) machines to improve performance."
|
||||
},
|
||||
{
|
||||
"author": "taabata",
|
||||
"title": "Syrian Falcon Nodes",
|
||||
@ -2254,6 +2355,106 @@
|
||||
"install_type": "copy",
|
||||
"description": "Nodes:Prompt editing, Word as Image"
|
||||
},
|
||||
{
|
||||
"author": "taabata",
|
||||
"title": "LCM_Inpaint-Outpaint_Comfy",
|
||||
"reference": "https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy",
|
||||
"files": [
|
||||
"https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom nodes for inpainting/outpainting using the new latent consistency model (LCM)"
|
||||
},
|
||||
{
|
||||
"author": "noxinias",
|
||||
"title": "ComfyUI_NoxinNodes",
|
||||
"reference": "https://github.com/noxinias/ComfyUI_NoxinNodes",
|
||||
"files": [
|
||||
"https://github.com/noxinias/ComfyUI_NoxinNodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Noxin Complete Chime, Noxin Scaled Resolutions, Load from Noxin Prompt Library, Save to Noxin Prompt Library"
|
||||
},
|
||||
{
|
||||
"author": "apesplat",
|
||||
"title": "ezXY scripts and nodes",
|
||||
"reference": "https://github.com/GMapeSplat/ComfyUI_ezXY",
|
||||
"files": [
|
||||
"https://github.com/GMapeSplat/ComfyUI_ezXY"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Extensions/Patches: Enables linking float and integer inputs and ouputs. Values are automatically cast to the correct type and clamped to the correct range. Works with both builtin and custom nodes.<p style='background-color: black; color: red;'>NOTE: This repo patches ComfyUI's validate_inputs and map_node_over_list functions while running. May break depending on your version of ComfyUI. Can be deactivated in config.yaml.</p>Nodes: A collection of nodes for facilitating the generation of XY plots. Capable of plotting changes over most primitive values."
|
||||
},
|
||||
{
|
||||
"author": "kinfolk0117",
|
||||
"title": "ComfyUI_SimpleTiles",
|
||||
"reference": "https://github.com/kinfolk0117/ComfyUI_SimpleTiles",
|
||||
"files": [
|
||||
"https://github.com/kinfolk0117/ComfyUI_SimpleTiles"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:TileSplit, TileMerge."
|
||||
},
|
||||
{
|
||||
"author": "Fictiverse",
|
||||
"title": "ComfyUI Fictiverse Nodes",
|
||||
"reference": "https://github.com/Fictiverse/ComfyUI_Fictiverse",
|
||||
"files": [
|
||||
"https://github.com/Fictiverse/ComfyUI_Fictiverse"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Color correction."
|
||||
},
|
||||
{
|
||||
"author": "idrirap",
|
||||
"title": "ComfyUI-Lora-Auto-Trigger-Words",
|
||||
"reference": "https://github.com/idrirap/ComfyUI-Lora-Auto-Trigger-Words",
|
||||
"files": [
|
||||
"https://github.com/idrirap/ComfyUI-Lora-Auto-Trigger-Words"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This project is a fork of <a href='https://github.com/Extraltodeus/LoadLoraWithTags'>https://github.com/Extraltodeus/LoadLoraWithTags</a> The aim of these custom nodes is to get an easy access to the tags used to trigger a lora."
|
||||
},
|
||||
{
|
||||
"author": "aianimation55",
|
||||
"title": "Comfy UI FatLabels",
|
||||
"reference": "https://github.com/aianimation55/ComfyUI-FatLabels",
|
||||
"files": [
|
||||
"https://github.com/aianimation55/ComfyUI-FatLabels"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "It's a super simple custom node for Comfy UI, to generate text, with a font size option. Useful for bigger labelling of nodes, helpful for wider screen captures or tutorials. Plus you can of course use the text within your generations."
|
||||
},
|
||||
{
|
||||
"author": "noEmbryo",
|
||||
"title": "ComfyUI-noEmbryo nodes",
|
||||
"reference": "https://github.com/noembryo/ComfyUI-noEmbryo",
|
||||
"files": [
|
||||
"https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/nodes.py"
|
||||
],
|
||||
"install_type": "copy",
|
||||
"description": "PromptTermList (1-6): are some nodes that help with the creation of Prompts inside ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "mikkel",
|
||||
"title": "ComfyUI - Mask Bounding Box",
|
||||
"reference": "https://github.com/mikkel/comfyui-mask-boundingbox",
|
||||
"files": [
|
||||
"https://github.com/mikkel/comfyui-mask-boundingbox"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "The ComfyUI Mask Bounding Box Plugin provides functionalities for selecting a specific size mask from an image. Can be combined with ClipSEG to replace any aspect of an SDXL image with an SD1.5 output."
|
||||
},
|
||||
{
|
||||
"author": "ParmanBabra",
|
||||
"title": "ComfyUI-Malefish-Custom-Scripts",
|
||||
"reference": "https://github.com/ParmanBabra/ComfyUI-Malefish-Custom-Scripts",
|
||||
"files": [
|
||||
"https://github.com/ParmanBabra/ComfyUI-Malefish-Custom-Scripts"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Multi Lora Loader, Random (Prompt), Combine (Prompt), CSV Prompts Loader"
|
||||
},
|
||||
{
|
||||
"author": "Ser-Hilary",
|
||||
"title": "SDXL_sizing",
|
||||
@ -2462,6 +2663,36 @@
|
||||
"install_type": "copy",
|
||||
"description": "Adds an Image Loader node that also shows images in subfolders of the default input directory"
|
||||
},
|
||||
{
|
||||
"author": "IAmMatan.com",
|
||||
"title": "ComfyUI Serving toolkit",
|
||||
"reference": "https://github.com/matan1905/ComfyUI-Serving-Toolkit",
|
||||
"files": [
|
||||
"https://github.com/matan1905/ComfyUI-Serving-Toolkit"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension adds nodes that allow you to easily serve your workflow (for example using a discord bot) "
|
||||
},
|
||||
{
|
||||
"author": "PCMonsterx",
|
||||
"title": "ComfyUI-CSV-Loader",
|
||||
"reference": "https://github.com/PCMonsterx/ComfyUI-CSV-Loader",
|
||||
"files": [
|
||||
"https://github.com/PCMonsterx/ComfyUI-CSV-Loader"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "CSV Loader for prompt building within ComfyUI interface. Allows access to positive/negative prompts associated with a name. Selections are being pulled from CSV files."
|
||||
},
|
||||
{
|
||||
"author": "Trung0246",
|
||||
"title": "ComfyUI-0246",
|
||||
"reference": "https://github.com/Trung0246/ComfyUI-0246",
|
||||
"files": [
|
||||
"https://github.com/Trung0246/ComfyUI-0246"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Highway. Random nodes for ComfyUI I made to solve my struggle with ComfyUI. Have varying quality."
|
||||
},
|
||||
{
|
||||
"author": "Smuzzies",
|
||||
"title": "Chatbox Overlay node for ComfyUI",
|
||||
@ -2472,6 +2703,16 @@
|
||||
"install_type": "copy",
|
||||
"description": "Nodes: Chatbox Overlay. Custom node for ComfyUI to add a text box over a processed image before save node."
|
||||
},
|
||||
{
|
||||
"author": "CaptainGrock",
|
||||
"title": "ComfyUIInvisibleWatermark",
|
||||
"reference": "https://github.com/CaptainGrock/ComfyUIInvisibleWatermark",
|
||||
"files": [
|
||||
"https://github.com/CaptainGrock/ComfyUIInvisibleWatermark/raw/main/Invisible%20Watermark.py"
|
||||
],
|
||||
"install_type": "copy",
|
||||
"description": "Nodes:Apply Invisible Watermark, Extract Watermark. Adds up to 12 characters encoded into an image that can be extracted."
|
||||
},
|
||||
{
|
||||
"author": "theally",
|
||||
"title": "TheAlly's Custom Nodes",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
212
git_helper.py
212
git_helper.py
@ -2,23 +2,53 @@ import sys
|
||||
import os
|
||||
import git
|
||||
import configparser
|
||||
import re
|
||||
import json
|
||||
from torchvision.datasets.utils import download_url
|
||||
from tqdm.auto import tqdm
|
||||
from git.remote import RemoteProgress
|
||||
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config.ini")
|
||||
nodelist_path = os.path.join(os.path.dirname(__file__), "custom-node-list.json")
|
||||
working_directory = os.getcwd()
|
||||
|
||||
def gitclone(custom_nodes_path, url):
|
||||
|
||||
class GitProgress(RemoteProgress):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pbar = tqdm(ascii=True)
|
||||
|
||||
def update(self, op_code, cur_count, max_count=None, message=''):
|
||||
self.pbar.total = max_count
|
||||
self.pbar.n = cur_count
|
||||
self.pbar.pos = 0
|
||||
self.pbar.refresh()
|
||||
|
||||
|
||||
def gitclone(custom_nodes_path, url, target_hash=None):
|
||||
repo_name = os.path.splitext(os.path.basename(url))[0]
|
||||
repo_path = os.path.join(custom_nodes_path, repo_name)
|
||||
|
||||
# Clone the repository from the remote URL
|
||||
repo = git.Repo.clone_from(url, repo_path, recursive=True)
|
||||
repo = git.Repo.clone_from(url, repo_path, recursive=True, progress=GitProgress())
|
||||
|
||||
if target_hash is not None:
|
||||
print(f"CHECKOUT: {repo_name} [{target_hash}]")
|
||||
repo.git.checkout(target_hash)
|
||||
|
||||
repo.git.clear_cache()
|
||||
repo.close()
|
||||
|
||||
|
||||
def gitcheck(path, do_fetch=False):
|
||||
try:
|
||||
# Fetch the latest commits from the remote repository
|
||||
repo = git.Repo(path)
|
||||
|
||||
if repo.head.is_detached:
|
||||
print("CUSTOM NODE CHECK: True")
|
||||
return
|
||||
|
||||
current_branch = repo.active_branch
|
||||
branch_name = current_branch.name
|
||||
|
||||
@ -48,6 +78,14 @@ def gitcheck(path, do_fetch=False):
|
||||
print("CUSTOM NODE CHECK: Error")
|
||||
|
||||
|
||||
def switch_to_default_branch(repo):
|
||||
show_result = repo.git.remote("show", "origin")
|
||||
matches = re.search(r"\s*HEAD branch:\s*(.*)", show_result)
|
||||
if matches:
|
||||
default_branch = matches.group(1)
|
||||
repo.git.checkout(default_branch)
|
||||
|
||||
|
||||
def gitpull(path):
|
||||
# Check if the path is a git repository
|
||||
if not os.path.exists(os.path.join(path, '.git')):
|
||||
@ -60,8 +98,12 @@ def gitpull(path):
|
||||
|
||||
commit_hash = repo.head.commit.hexsha
|
||||
try:
|
||||
if repo.head.is_detached:
|
||||
switch_to_default_branch(repo)
|
||||
|
||||
origin = repo.remote(name='origin')
|
||||
origin.pull(rebase=True)
|
||||
origin.pull()
|
||||
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
new_commit_hash = repo.head.commit.hexsha
|
||||
|
||||
@ -76,6 +118,165 @@ def gitpull(path):
|
||||
repo.close()
|
||||
|
||||
|
||||
def checkout_comfyui_hash(target_hash):
|
||||
repo_path = os.path.join(working_directory, '..') # ComfyUI dir
|
||||
|
||||
repo = git.Repo(repo_path)
|
||||
commit_hash = repo.head.commit.hexsha
|
||||
|
||||
if commit_hash != target_hash:
|
||||
try:
|
||||
print(f"CHECKOUT: ComfyUI [{target_hash}]")
|
||||
repo.git.checkout(target_hash)
|
||||
except git.GitCommandError as e:
|
||||
print(f"Error checking out the ComfyUI: {str(e)}")
|
||||
|
||||
|
||||
def checkout_custom_node_hash(git_custom_node_infos):
|
||||
repo_name_to_url = {}
|
||||
|
||||
for url in git_custom_node_infos.keys():
|
||||
repo_name = url.split('/')[-1]
|
||||
|
||||
if repo_name.endswith('.git'):
|
||||
repo_name = repo_name[:-4]
|
||||
|
||||
repo_name_to_url[repo_name] = url
|
||||
|
||||
for path in os.listdir(working_directory):
|
||||
if path.endswith("ComfyUI-Manager"):
|
||||
continue
|
||||
|
||||
fullpath = os.path.join(working_directory, path)
|
||||
|
||||
if os.path.isdir(fullpath):
|
||||
is_disabled = path.endswith(".disabled")
|
||||
|
||||
try:
|
||||
git_dir = os.path.join(fullpath, '.git')
|
||||
if not os.path.exists(git_dir):
|
||||
continue
|
||||
|
||||
need_checkout = False
|
||||
repo_name = os.path.basename(fullpath)
|
||||
|
||||
if repo_name.endswith('.disabled'):
|
||||
repo_name = repo_name[:-9]
|
||||
|
||||
item = git_custom_node_infos[repo_name_to_url[repo_name]]
|
||||
if item['disabled'] and is_disabled:
|
||||
pass
|
||||
elif item['disabled'] and not is_disabled:
|
||||
# disable
|
||||
print(f"DISABLE: {repo_name}")
|
||||
new_path = fullpath + ".disabled"
|
||||
os.rename(fullpath, new_path)
|
||||
pass
|
||||
elif not item['disabled'] and is_disabled:
|
||||
# enable
|
||||
print(f"ENABLE: {repo_name}")
|
||||
new_path = fullpath[:-9]
|
||||
os.rename(fullpath, new_path)
|
||||
fullpath = new_path
|
||||
need_checkout = True
|
||||
else:
|
||||
need_checkout = True
|
||||
|
||||
if need_checkout:
|
||||
repo = git.Repo(fullpath)
|
||||
commit_hash = repo.head.commit.hexsha
|
||||
|
||||
if commit_hash != item['hash']:
|
||||
print(f"CHECKOUT: {repo_name} [{item['hash']}]")
|
||||
repo.git.checkout(item['hash'])
|
||||
except Exception:
|
||||
print(f"Failed to restore snapshots for the custom node '{path}'")
|
||||
|
||||
# clone missing
|
||||
for k, v in git_custom_node_infos.items():
|
||||
if not v['disabled']:
|
||||
repo_name = k.split('/')[-1]
|
||||
if repo_name.endswith('.git'):
|
||||
repo_name = repo_name[:-4]
|
||||
|
||||
path = os.path.join(working_directory, repo_name)
|
||||
if not os.path.exists(path):
|
||||
print(f"CLONE: {path}")
|
||||
gitclone(working_directory, k, v['hash'])
|
||||
|
||||
|
||||
def invalidate_custom_node_file(file_custom_node_infos):
|
||||
global nodelist_path
|
||||
|
||||
enabled_set = set()
|
||||
for item in file_custom_node_infos:
|
||||
if not item['disabled']:
|
||||
enabled_set.add(item['filename'])
|
||||
|
||||
for path in os.listdir(working_directory):
|
||||
fullpath = os.path.join(working_directory, path)
|
||||
|
||||
if not os.path.isdir(fullpath) and fullpath.endswith('.py'):
|
||||
if path not in enabled_set:
|
||||
print(f"DISABLE: {path}")
|
||||
new_path = fullpath+'.disabled'
|
||||
os.rename(fullpath, new_path)
|
||||
|
||||
elif not os.path.isdir(fullpath) and fullpath.endswith('.py.disabled'):
|
||||
path = path[:-9]
|
||||
if path in enabled_set:
|
||||
print(f"ENABLE: {path}")
|
||||
new_path = fullpath[:-9]
|
||||
os.rename(fullpath, new_path)
|
||||
|
||||
# download missing: just support for 'copy' style
|
||||
py_to_url = {}
|
||||
|
||||
with open(nodelist_path, 'r', encoding="UTF-8") as json_file:
|
||||
info = json.load(json_file)
|
||||
for item in info['custom_nodes']:
|
||||
if item['install_type'] == 'copy':
|
||||
for url in item['files']:
|
||||
if url.endswith('.py'):
|
||||
py = url.split('/')[-1]
|
||||
py_to_url[py] = url
|
||||
|
||||
for item in file_custom_node_infos:
|
||||
filename = item['filename']
|
||||
if not item['disabled']:
|
||||
target_path = os.path.join(working_directory, filename)
|
||||
|
||||
if not os.path.exists(target_path) and filename in py_to_url:
|
||||
url = py_to_url[filename]
|
||||
print(f"DOWNLOAD: {filename}")
|
||||
download_url(url, working_directory)
|
||||
|
||||
|
||||
def apply_snapshot(target):
|
||||
try:
|
||||
path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}")
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r', encoding="UTF-8") as json_file:
|
||||
info = json.load(json_file)
|
||||
|
||||
comfyui_hash = info['comfyui']
|
||||
git_custom_node_infos = info['git_custom_nodes']
|
||||
file_custom_node_infos = info['file_custom_nodes']
|
||||
|
||||
checkout_comfyui_hash(comfyui_hash)
|
||||
checkout_custom_node_hash(git_custom_node_infos)
|
||||
invalidate_custom_node_file(file_custom_node_infos)
|
||||
|
||||
print("APPLY SNAPSHOT: True")
|
||||
return
|
||||
|
||||
print(f"Snapshot file not found: `{path}`")
|
||||
print("APPLY SNAPSHOT: False")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("APPLY SNAPSHOT: False")
|
||||
|
||||
|
||||
def setup_environment():
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
@ -95,8 +296,11 @@ try:
|
||||
gitcheck(sys.argv[2], True)
|
||||
elif sys.argv[1] == "--pull":
|
||||
gitpull(sys.argv[2])
|
||||
elif sys.argv[1] == "--apply-snapshot":
|
||||
apply_snapshot(sys.argv[2])
|
||||
sys.exit(0)
|
||||
except:
|
||||
except Exception as e:
|
||||
print(e)
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
|
||||
554
js/a1111-alter-downloader.js
Normal file
554
js/a1111-alter-downloader.js
Normal file
@ -0,0 +1,554 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { install_checked_custom_node, manager_instance } from "./common.js";
|
||||
|
||||
async function getAlterList() {
|
||||
var mode = "url";
|
||||
if(manager_instance.local_mode_checkbox.checked)
|
||||
mode = "local";
|
||||
|
||||
var skip_update = "";
|
||||
if(manager_instance.update_check_checkbox.checked)
|
||||
skip_update = "&skip_update=true";
|
||||
|
||||
const response = await api.fetchApi(`/alternatives/getlist?mode=${mode}${skip_update}`);
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export class AlternativesInstaller extends ComfyDialog {
|
||||
static instance = null;
|
||||
|
||||
install_buttons = [];
|
||||
message_box = null;
|
||||
data = null;
|
||||
|
||||
clear() {
|
||||
this.install_buttons = [];
|
||||
this.message_box = null;
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.search_keyword = '';
|
||||
this.element = $el("div.comfy-modal", { parent: document.body }, []);
|
||||
}
|
||||
|
||||
startInstall(target) {
|
||||
const self = AlternativesInstaller.instance;
|
||||
|
||||
self.updateMessage(`<BR><font color="green">Installing '${target.title}'</font>`);
|
||||
}
|
||||
|
||||
disableButtons() {
|
||||
for(let i in this.install_buttons) {
|
||||
this.install_buttons[i].disabled = true;
|
||||
this.install_buttons[i].style.backgroundColor = 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
apply_searchbox(data) {
|
||||
let keyword = this.search_box.value.toLowerCase();
|
||||
for(let i in this.grid_rows) {
|
||||
let data1 = this.grid_rows[i].data;
|
||||
let data2 = data1.custom_node;
|
||||
|
||||
if(!data2)
|
||||
continue;
|
||||
|
||||
let content = data1.tags.toLowerCase() + data1.description.toLowerCase() + data2.author.toLowerCase() + data2.description.toLowerCase() + data2.title.toLowerCase();
|
||||
|
||||
if(this.filter && this.filter != '*') {
|
||||
if(this.filter != data2.installed) {
|
||||
this.grid_rows[i].control.style.display = 'none';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(keyword == "")
|
||||
this.grid_rows[i].control.style.display = null;
|
||||
else if(content.includes(keyword)) {
|
||||
this.grid_rows[i].control.style.display = null;
|
||||
}
|
||||
else {
|
||||
this.grid_rows[i].control.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async invalidateControl() {
|
||||
this.clear();
|
||||
|
||||
// splash
|
||||
while (this.element.children.length) {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
}
|
||||
|
||||
const msg = $el('div', {id:'custom-message'},
|
||||
[$el('br'),
|
||||
'The custom node DB is currently being updated, and updates to custom nodes are being checked for.',
|
||||
$el('br'),
|
||||
'NOTE: Update only checks for extensions that have been fetched.',
|
||||
$el('br')]);
|
||||
msg.style.height = '100px';
|
||||
msg.style.verticalAlign = 'middle';
|
||||
this.element.appendChild(msg);
|
||||
|
||||
// invalidate
|
||||
this.data = (await getAlterList()).items;
|
||||
|
||||
this.element.removeChild(msg);
|
||||
|
||||
while (this.element.children.length) {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
}
|
||||
|
||||
this.createHeaderControls();
|
||||
await this.createGrid();
|
||||
this.apply_searchbox(this.data);
|
||||
this.createBottomControls();
|
||||
}
|
||||
|
||||
updateMessage(msg) {
|
||||
this.message_box.innerHTML = msg;
|
||||
}
|
||||
|
||||
invalidate_checks(is_checked, install_state) {
|
||||
if(is_checked) {
|
||||
for(let i in this.grid_rows) {
|
||||
let data = this.grid_rows[i].data;
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
let buttons = this.grid_rows[i].buttons;
|
||||
|
||||
checkbox.disabled = data.custom_node.installed != install_state;
|
||||
|
||||
if(checkbox.disabled) {
|
||||
for(let j in buttons) {
|
||||
buttons[j].style.display = 'none';
|
||||
}
|
||||
}
|
||||
else {
|
||||
for(let j in buttons) {
|
||||
buttons[j].style.display = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.checkbox_all.disabled = false;
|
||||
}
|
||||
else {
|
||||
for(let i in this.grid_rows) {
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
if(checkbox.check)
|
||||
return; // do nothing
|
||||
}
|
||||
|
||||
// every checkbox is unchecked -> enable all checkbox
|
||||
for(let i in this.grid_rows) {
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
let buttons = this.grid_rows[i].buttons;
|
||||
checkbox.disabled = false;
|
||||
|
||||
for(let j in buttons) {
|
||||
buttons[j].style.display = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.checkbox_all.checked = false;
|
||||
this.checkbox_all.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
check_all(is_checked) {
|
||||
if(is_checked) {
|
||||
// lookup first checked item's state
|
||||
let check_state = null;
|
||||
for(let i in this.grid_rows) {
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
if(checkbox.checked) {
|
||||
check_state = this.grid_rows[i].data.custom_node.installed;
|
||||
}
|
||||
}
|
||||
|
||||
if(check_state == null)
|
||||
return;
|
||||
|
||||
// check only same state items
|
||||
for(let i in this.grid_rows) {
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
if(this.grid_rows[i].data.custom_node.installed == check_state)
|
||||
checkbox.checked = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// uncheck all
|
||||
for(let i in this.grid_rows) {
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
let buttons = this.grid_rows[i].buttons;
|
||||
checkbox.checked = false;
|
||||
checkbox.disabled = false;
|
||||
|
||||
for(let j in buttons) {
|
||||
buttons[j].style.display = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.checkbox_all.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
async createGrid() {
|
||||
var grid = document.createElement('table');
|
||||
grid.setAttribute('id', 'alternatives-grid');
|
||||
|
||||
this.grid_rows = {};
|
||||
|
||||
let self = this;
|
||||
|
||||
var thead = document.createElement('thead');
|
||||
var tbody = document.createElement('tbody');
|
||||
|
||||
var headerRow = document.createElement('tr');
|
||||
thead.style.position = "sticky";
|
||||
thead.style.top = "0px";
|
||||
thead.style.borderCollapse = "collapse";
|
||||
thead.style.tableLayout = "fixed";
|
||||
|
||||
var header0 = document.createElement('th');
|
||||
header0.style.width = "20px";
|
||||
this.checkbox_all = $el("input",{type:'checkbox', id:'check_all'},[]);
|
||||
header0.appendChild(this.checkbox_all);
|
||||
this.checkbox_all.checked = false;
|
||||
this.checkbox_all.disabled = true;
|
||||
this.checkbox_all.addEventListener('change', function() { self.check_all.call(self, self.checkbox_all.checked); });
|
||||
|
||||
var header1 = document.createElement('th');
|
||||
header1.innerHTML = ' ID ';
|
||||
header1.style.width = "20px";
|
||||
var header2 = document.createElement('th');
|
||||
header2.innerHTML = 'Tags';
|
||||
header2.style.width = "10%";
|
||||
var header3 = document.createElement('th');
|
||||
header3.innerHTML = 'Author';
|
||||
header3.style.width = "150px";
|
||||
var header4 = document.createElement('th');
|
||||
header4.innerHTML = 'Title';
|
||||
header4.style.width = "20%";
|
||||
var header5 = document.createElement('th');
|
||||
header5.innerHTML = 'Description';
|
||||
header5.style.width = "50%";
|
||||
var header6 = document.createElement('th');
|
||||
header6.innerHTML = 'Install';
|
||||
header6.style.width = "130px";
|
||||
|
||||
header1.style.position = "sticky";
|
||||
header1.style.top = "0px";
|
||||
header2.style.position = "sticky";
|
||||
header2.style.top = "0px";
|
||||
header3.style.position = "sticky";
|
||||
header3.style.top = "0px";
|
||||
header4.style.position = "sticky";
|
||||
header4.style.top = "0px";
|
||||
header5.style.position = "sticky";
|
||||
header5.style.top = "0px";
|
||||
|
||||
thead.appendChild(headerRow);
|
||||
headerRow.appendChild(header0);
|
||||
headerRow.appendChild(header1);
|
||||
headerRow.appendChild(header2);
|
||||
headerRow.appendChild(header3);
|
||||
headerRow.appendChild(header4);
|
||||
headerRow.appendChild(header5);
|
||||
headerRow.appendChild(header6);
|
||||
|
||||
headerRow.style.backgroundColor = "Black";
|
||||
headerRow.style.color = "White";
|
||||
headerRow.style.textAlign = "center";
|
||||
headerRow.style.width = "100%";
|
||||
headerRow.style.padding = "0";
|
||||
|
||||
grid.appendChild(thead);
|
||||
grid.appendChild(tbody);
|
||||
|
||||
if(this.data)
|
||||
for (var i = 0; i < this.data.length; i++) {
|
||||
const data = this.data[i];
|
||||
var dataRow = document.createElement('tr');
|
||||
|
||||
let data0 = document.createElement('td');
|
||||
let checkbox = $el("input",{type:'checkbox', id:`check_${i}`},[]);
|
||||
data0.appendChild(checkbox);
|
||||
checkbox.checked = false;
|
||||
checkbox.addEventListener('change', function() { self.invalidate_checks.call(self, checkbox.checked, data.custom_node?.installed); });
|
||||
|
||||
var data1 = document.createElement('td');
|
||||
data1.style.textAlign = "center";
|
||||
data1.innerHTML = i+1;
|
||||
var data2 = document.createElement('td');
|
||||
data2.innerHTML = ` ${data.tags}`;
|
||||
var data3 = document.createElement('td');
|
||||
var data4 = document.createElement('td');
|
||||
if(data.custom_node) {
|
||||
data3.innerHTML = ` ${data.custom_node.author}`;
|
||||
data4.innerHTML = ` <a href=${data.custom_node.reference} target="_blank"><font color="skyblue"><b>${data.custom_node.title}</b></font></a>`;
|
||||
}
|
||||
else {
|
||||
data3.innerHTML = ` Unknown`;
|
||||
data4.innerHTML = ` Unknown`;
|
||||
}
|
||||
var data5 = document.createElement('td');
|
||||
data5.innerHTML = data.description;
|
||||
var data6 = document.createElement('td');
|
||||
data6.style.textAlign = "center";
|
||||
|
||||
var installBtn = document.createElement('button');
|
||||
var installBtn2 = null;
|
||||
var installBtn3 = null;
|
||||
|
||||
if(data.custom_node) {
|
||||
this.install_buttons.push(installBtn);
|
||||
|
||||
switch(data.custom_node.installed) {
|
||||
case 'Disabled':
|
||||
installBtn3 = document.createElement('button');
|
||||
installBtn3.innerHTML = 'Enable';
|
||||
installBtn3.style.backgroundColor = 'blue';
|
||||
installBtn3.style.color = 'white';
|
||||
this.install_buttons.push(installBtn3);
|
||||
|
||||
installBtn.innerHTML = 'Uninstall';
|
||||
installBtn.style.backgroundColor = 'red';
|
||||
installBtn.style.color = 'white';
|
||||
break;
|
||||
case 'Update':
|
||||
installBtn2 = document.createElement('button');
|
||||
installBtn2.innerHTML = 'Update';
|
||||
installBtn2.style.backgroundColor = 'blue';
|
||||
installBtn2.style.color = 'white';
|
||||
this.install_buttons.push(installBtn2);
|
||||
|
||||
installBtn3 = document.createElement('button');
|
||||
installBtn3.innerHTML = 'Disable';
|
||||
installBtn3.style.backgroundColor = 'MediumSlateBlue';
|
||||
installBtn3.style.color = 'white';
|
||||
this.install_buttons.push(installBtn3);
|
||||
|
||||
installBtn.innerHTML = 'Uninstall';
|
||||
installBtn.style.backgroundColor = 'red';
|
||||
installBtn.style.color = 'white';
|
||||
break;
|
||||
case 'True':
|
||||
installBtn3 = document.createElement('button');
|
||||
installBtn3.innerHTML = 'Disable';
|
||||
installBtn3.style.backgroundColor = 'MediumSlateBlue';
|
||||
installBtn3.style.color = 'white';
|
||||
this.install_buttons.push(installBtn3);
|
||||
|
||||
installBtn.innerHTML = 'Uninstall';
|
||||
installBtn.style.backgroundColor = 'red';
|
||||
installBtn.style.color = 'white';
|
||||
break;
|
||||
case 'False':
|
||||
installBtn.innerHTML = 'Install';
|
||||
installBtn.style.backgroundColor = 'black';
|
||||
installBtn.style.color = 'white';
|
||||
break;
|
||||
default:
|
||||
installBtn.innerHTML = 'Try Install';
|
||||
installBtn.style.backgroundColor = 'Gray';
|
||||
installBtn.style.color = 'white';
|
||||
}
|
||||
|
||||
let j = i;
|
||||
if(installBtn2 != null) {
|
||||
installBtn2.style.width = "120px";
|
||||
installBtn2.addEventListener('click', function() {
|
||||
install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'update');
|
||||
});
|
||||
|
||||
data6.appendChild(installBtn2);
|
||||
}
|
||||
|
||||
if(installBtn3 != null) {
|
||||
installBtn3.style.width = "120px";
|
||||
installBtn3.addEventListener('click', function() {
|
||||
install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'toggle_active');
|
||||
});
|
||||
|
||||
data6.appendChild(installBtn3);
|
||||
}
|
||||
|
||||
|
||||
installBtn.style.width = "120px";
|
||||
installBtn.addEventListener('click', function() {
|
||||
if(this.innerHTML == 'Uninstall') {
|
||||
if (confirm(`Are you sure uninstall ${data.title}?`)) {
|
||||
install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'uninstall');
|
||||
}
|
||||
}
|
||||
else {
|
||||
install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'install');
|
||||
}
|
||||
});
|
||||
|
||||
data6.appendChild(installBtn);
|
||||
}
|
||||
|
||||
dataRow.style.backgroundColor = "var(--bg-color)";
|
||||
dataRow.style.color = "var(--fg-color)";
|
||||
dataRow.style.textAlign = "left";
|
||||
|
||||
dataRow.appendChild(data0);
|
||||
dataRow.appendChild(data1);
|
||||
dataRow.appendChild(data2);
|
||||
dataRow.appendChild(data3);
|
||||
dataRow.appendChild(data4);
|
||||
dataRow.appendChild(data5);
|
||||
dataRow.appendChild(data6);
|
||||
tbody.appendChild(dataRow);
|
||||
|
||||
let buttons = [];
|
||||
if(installBtn) {
|
||||
buttons.push(installBtn);
|
||||
}
|
||||
if(installBtn2) {
|
||||
buttons.push(installBtn2);
|
||||
}
|
||||
if(installBtn3) {
|
||||
buttons.push(installBtn3);
|
||||
}
|
||||
|
||||
this.grid_rows[i] = {data:data, buttons:buttons, checkbox:checkbox, control:dataRow};
|
||||
}
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.style.width = "100%";
|
||||
panel.appendChild(grid);
|
||||
|
||||
function handleResize() {
|
||||
const parentHeight = self.element.clientHeight;
|
||||
const gridHeight = parentHeight - 200;
|
||||
|
||||
grid.style.height = gridHeight + "px";
|
||||
}
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
grid.style.position = "relative";
|
||||
grid.style.display = "inline-block";
|
||||
grid.style.width = "100%";
|
||||
grid.style.height = "100%";
|
||||
grid.style.overflowY = "scroll";
|
||||
this.element.style.height = "85%";
|
||||
this.element.style.width = "80%";
|
||||
this.element.appendChild(panel);
|
||||
|
||||
handleResize();
|
||||
}
|
||||
|
||||
createFilterCombo() {
|
||||
let combo = document.createElement("select");
|
||||
|
||||
combo.style.cssFloat = "left";
|
||||
combo.style.fontSize = "14px";
|
||||
combo.style.padding = "4px";
|
||||
combo.style.background = "black";
|
||||
combo.style.marginLeft = "2px";
|
||||
combo.style.width = "199px";
|
||||
combo.id = `combo-manger-filter`;
|
||||
combo.style.borderRadius = "15px";
|
||||
|
||||
let items =
|
||||
[
|
||||
{ value:'*', text:'Filter: all' },
|
||||
{ value:'Disabled', text:'Filter: disabled' },
|
||||
{ value:'Update', text:'Filter: update' },
|
||||
{ value:'True', text:'Filter: installed' },
|
||||
{ value:'False', text:'Filter: not-installed' },
|
||||
];
|
||||
|
||||
items.forEach(item => {
|
||||
const option = document.createElement("option");
|
||||
option.value = item.value;
|
||||
option.text = item.text;
|
||||
combo.appendChild(option);
|
||||
});
|
||||
|
||||
let self = this;
|
||||
combo.addEventListener('change', function(event) {
|
||||
self.filter = event.target.value;
|
||||
self.apply_searchbox();
|
||||
});
|
||||
|
||||
if(self.filter) {
|
||||
combo.value = self.filter;
|
||||
}
|
||||
|
||||
return combo;
|
||||
}
|
||||
|
||||
createHeaderControls() {
|
||||
let self = this;
|
||||
this.search_box = $el('input', {type:'text', id:'manager-alternode-search-box', placeholder:'input search keyword', value:this.search_keyword}, []);
|
||||
this.search_box.style.height = "25px";
|
||||
this.search_box.onkeydown = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
self.search_keyword = self.search_box.value;
|
||||
self.apply_searchbox();
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
self.search_keyword = self.search_box.value;
|
||||
self.apply_searchbox();
|
||||
}
|
||||
};
|
||||
|
||||
let search_button = document.createElement("button");
|
||||
search_button.innerHTML = "Search";
|
||||
search_button.onclick = () => {
|
||||
self.search_keyword = self.search_box.value;
|
||||
self.apply_searchbox();
|
||||
};
|
||||
search_button.style.display = "inline-block";
|
||||
|
||||
let filter_control = this.createFilterCombo();
|
||||
filter_control.style.display = "inline-block";
|
||||
|
||||
let cell = $el('td', {width:'100%'}, [filter_control, this.search_box, ' ', search_button]);
|
||||
let search_control = $el('table', {width:'100%'},
|
||||
[
|
||||
$el('tr', {}, [cell])
|
||||
]
|
||||
);
|
||||
|
||||
cell.style.textAlign = "right";
|
||||
this.element.appendChild(search_control);
|
||||
}
|
||||
|
||||
async createBottomControls() {
|
||||
var close_button = document.createElement("button");
|
||||
close_button.innerHTML = "Close";
|
||||
close_button.onclick = () => { this.close(); }
|
||||
close_button.style.display = "inline-block";
|
||||
|
||||
this.message_box = $el('div', {id:'alternatives-installer-message'}, [$el('br'), '']);
|
||||
this.message_box.style.height = '60px';
|
||||
this.message_box.style.verticalAlign = 'middle';
|
||||
|
||||
this.element.appendChild(this.message_box);
|
||||
this.element.appendChild(close_button);
|
||||
}
|
||||
|
||||
async show() {
|
||||
try {
|
||||
this.invalidateControl();
|
||||
this.element.style.display = "block";
|
||||
this.element.style.zIndex = 10001;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Failed to get alternatives list. / ${exception}`);
|
||||
console.error(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
62
js/common.js
Normal file
62
js/common.js
Normal file
@ -0,0 +1,62 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
|
||||
export async function install_checked_custom_node(grid_rows, target_i, caller, mode) {
|
||||
if(caller) {
|
||||
let failed = '';
|
||||
|
||||
caller.disableButtons();
|
||||
|
||||
for(let i in grid_rows) {
|
||||
if(!grid_rows[i].checkbox.checked && i != target_i)
|
||||
continue;
|
||||
|
||||
var target;
|
||||
|
||||
if(grid_rows[i].data.custom_node) {
|
||||
target = grid_rows[i].data.custom_node;
|
||||
}
|
||||
else {
|
||||
target = grid_rows[i].data;
|
||||
}
|
||||
|
||||
caller.startInstall(target);
|
||||
|
||||
try {
|
||||
const response = await api.fetchApi(`/customnode/${mode}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(target)
|
||||
});
|
||||
|
||||
if(response.status == 400) {
|
||||
app.ui.dialog.show(`${mode} failed: ${target.title}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
continue;
|
||||
}
|
||||
|
||||
const status = await response.json();
|
||||
app.ui.dialog.close();
|
||||
target.installed = 'True';
|
||||
continue;
|
||||
}
|
||||
catch(exception) {
|
||||
failed += `<BR> ${target.title}`;
|
||||
}
|
||||
}
|
||||
|
||||
if(failed != '') {
|
||||
app.ui.dialog.show(`${mode} failed: ${failed}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
}
|
||||
|
||||
await caller.invalidateControl();
|
||||
caller.updateMessage('<BR>To apply the installed/disabled/enabled custom node, please restart ComfyUI.');
|
||||
}
|
||||
};
|
||||
|
||||
export var manager_instance = null;
|
||||
|
||||
export function setManagerInstance(obj) {
|
||||
manager_instance = obj;
|
||||
}
|
||||
638
js/custom-nodes-downloader.js
Normal file
638
js/custom-nodes-downloader.js
Normal file
@ -0,0 +1,638 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { install_checked_custom_node, manager_instance } from "./common.js";
|
||||
|
||||
async function getCustomNodes() {
|
||||
var mode = "url";
|
||||
if(manager_instance.local_mode_checkbox.checked)
|
||||
mode = "local";
|
||||
|
||||
var skip_update = "";
|
||||
if(manager_instance.update_check_checkbox.checked)
|
||||
skip_update = "&skip_update=true";
|
||||
|
||||
const response = await api.fetchApi(`/customnode/getlist?mode=${mode}${skip_update}`);
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getCustomnodeMappings() {
|
||||
var mode = "url";
|
||||
if(manager_instance.local_mode_checkbox.checked)
|
||||
mode = "local";
|
||||
|
||||
const response = await api.fetchApi(`/customnode/getmappings?mode=${mode}`);
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getUnresolvedNodesInComponent() {
|
||||
try {
|
||||
var mode = "url";
|
||||
if(manager_instance.local_mode_checkbox.checked)
|
||||
mode = "local";
|
||||
|
||||
const response = await api.fetchApi(`/component/get_unresolved`);
|
||||
|
||||
const data = await response.json();
|
||||
return data.nodes;
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomNodesInstaller extends ComfyDialog {
|
||||
static instance = null;
|
||||
|
||||
install_buttons = [];
|
||||
message_box = null;
|
||||
data = null;
|
||||
|
||||
clear() {
|
||||
this.install_buttons = [];
|
||||
this.message_box = null;
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.search_keyword = '';
|
||||
this.element = $el("div.comfy-modal", { parent: document.body }, []);
|
||||
}
|
||||
|
||||
startInstall(target) {
|
||||
const self = CustomNodesInstaller.instance;
|
||||
|
||||
self.updateMessage(`<BR><font color="green">Installing '${target.title}'</font>`);
|
||||
}
|
||||
|
||||
disableButtons() {
|
||||
for(let i in this.install_buttons) {
|
||||
this.install_buttons[i].disabled = true;
|
||||
this.install_buttons[i].style.backgroundColor = 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
apply_searchbox(data) {
|
||||
let keyword = this.search_box.value.toLowerCase();
|
||||
for(let i in this.grid_rows) {
|
||||
let data = this.grid_rows[i].data;
|
||||
let content = data.author.toLowerCase() + data.description.toLowerCase() + data.title.toLowerCase();
|
||||
|
||||
if(this.filter && this.filter != '*') {
|
||||
if(this.filter != data.installed) {
|
||||
this.grid_rows[i].control.style.display = 'none';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(keyword == "")
|
||||
this.grid_rows[i].control.style.display = null;
|
||||
else if(content.includes(keyword)) {
|
||||
this.grid_rows[i].control.style.display = null;
|
||||
}
|
||||
else {
|
||||
this.grid_rows[i].control.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async filter_missing_node(data) {
|
||||
const mappings = await getCustomnodeMappings();
|
||||
|
||||
|
||||
// build regex->url map
|
||||
const regex_to_url = [];
|
||||
for (let i in data) {
|
||||
if(data[i]['nodename_pattern']) {
|
||||
let item = {regex: new RegExp(data[i].nodename_pattern), url: data[i].files[0]};
|
||||
regex_to_url.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// build name->url map
|
||||
const name_to_url = {};
|
||||
for (const url in mappings) {
|
||||
const names = mappings[url];
|
||||
for(const name in names[0]) {
|
||||
name_to_url[names[0][name]] = url;
|
||||
}
|
||||
}
|
||||
|
||||
const registered_nodes = new Set();
|
||||
for (let i in LiteGraph.registered_node_types) {
|
||||
registered_nodes.add(LiteGraph.registered_node_types[i].type);
|
||||
}
|
||||
|
||||
const missing_nodes = new Set();
|
||||
const nodes = app.graph.serialize().nodes;
|
||||
for (let i in nodes) {
|
||||
const node_type = nodes[i].type;
|
||||
if (!registered_nodes.has(node_type)) {
|
||||
const url = name_to_url[node_type.trim()];
|
||||
if(url)
|
||||
missing_nodes.add(url);
|
||||
else {
|
||||
for(let j in regex_to_url) {
|
||||
if(regex_to_url[j].regex.test(node_type)) {
|
||||
missing_nodes.add(regex_to_url[j].url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let unresolved_nodes = await getUnresolvedNodesInComponent();
|
||||
for (let i in unresolved_nodes) {
|
||||
let node_type = unresolved_nodes[i];
|
||||
const url = name_to_url[node_type];
|
||||
if(url)
|
||||
missing_nodes.add(url);
|
||||
}
|
||||
|
||||
return data.filter(node => node.files.some(file => missing_nodes.has(file)));
|
||||
}
|
||||
|
||||
async invalidateControl() {
|
||||
this.clear();
|
||||
|
||||
// splash
|
||||
while (this.element.children.length) {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
}
|
||||
|
||||
const msg = $el('div', {id:'custom-message'},
|
||||
[$el('br'),
|
||||
'The custom node DB is currently being updated, and updates to custom nodes are being checked for.',
|
||||
$el('br'),
|
||||
'NOTE: Update only checks for extensions that have been fetched.',
|
||||
$el('br')]);
|
||||
msg.style.height = '100px';
|
||||
msg.style.verticalAlign = 'middle';
|
||||
msg.style.color = "var(--fg-color)";
|
||||
|
||||
this.element.appendChild(msg);
|
||||
|
||||
// invalidate
|
||||
this.data = (await getCustomNodes()).custom_nodes;
|
||||
|
||||
if(this.is_missing_node_mode)
|
||||
this.data = await this.filter_missing_node(this.data);
|
||||
|
||||
this.element.removeChild(msg);
|
||||
|
||||
while (this.element.children.length) {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
}
|
||||
|
||||
this.createHeaderControls();
|
||||
await this.createGrid();
|
||||
this.apply_searchbox(this.data);
|
||||
this.createBottomControls();
|
||||
}
|
||||
|
||||
updateMessage(msg) {
|
||||
this.message_box.innerHTML = msg;
|
||||
}
|
||||
|
||||
invalidate_checks(is_checked, install_state) {
|
||||
if(is_checked) {
|
||||
for(let i in this.grid_rows) {
|
||||
let data = this.grid_rows[i].data;
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
let buttons = this.grid_rows[i].buttons;
|
||||
|
||||
checkbox.disabled = data.installed != install_state;
|
||||
|
||||
if(checkbox.disabled) {
|
||||
for(let j in buttons) {
|
||||
buttons[j].style.display = 'none';
|
||||
}
|
||||
}
|
||||
else {
|
||||
for(let j in buttons) {
|
||||
buttons[j].style.display = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.checkbox_all.disabled = false;
|
||||
}
|
||||
else {
|
||||
for(let i in this.grid_rows) {
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
if(checkbox.check)
|
||||
return; // do nothing
|
||||
}
|
||||
|
||||
// every checkbox is unchecked -> enable all checkbox
|
||||
for(let i in this.grid_rows) {
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
let buttons = this.grid_rows[i].buttons;
|
||||
checkbox.disabled = false;
|
||||
|
||||
for(let j in buttons) {
|
||||
buttons[j].style.display = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.checkbox_all.checked = false;
|
||||
this.checkbox_all.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
check_all(is_checked) {
|
||||
if(is_checked) {
|
||||
// lookup first checked item's state
|
||||
let check_state = null;
|
||||
for(let i in this.grid_rows) {
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
if(checkbox.checked) {
|
||||
check_state = this.grid_rows[i].data.installed;
|
||||
}
|
||||
}
|
||||
|
||||
if(check_state == null)
|
||||
return;
|
||||
|
||||
// check only same state items
|
||||
for(let i in this.grid_rows) {
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
if(this.grid_rows[i].data.installed == check_state)
|
||||
checkbox.checked = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// uncheck all
|
||||
for(let i in this.grid_rows) {
|
||||
let checkbox = this.grid_rows[i].checkbox;
|
||||
let buttons = this.grid_rows[i].buttons;
|
||||
checkbox.checked = false;
|
||||
checkbox.disabled = false;
|
||||
|
||||
for(let j in buttons) {
|
||||
buttons[j].style.display = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.checkbox_all.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
async createGrid() {
|
||||
var grid = document.createElement('table');
|
||||
grid.setAttribute('id', 'custom-nodes-grid');
|
||||
|
||||
this.grid_rows = {};
|
||||
|
||||
let self = this;
|
||||
|
||||
var thead = document.createElement('thead');
|
||||
var tbody = document.createElement('tbody');
|
||||
|
||||
var headerRow = document.createElement('tr');
|
||||
thead.style.position = "sticky";
|
||||
thead.style.top = "0px";
|
||||
thead.style.borderCollapse = "collapse";
|
||||
thead.style.tableLayout = "fixed";
|
||||
|
||||
var header0 = document.createElement('th');
|
||||
header0.style.width = "20px";
|
||||
this.checkbox_all = $el("input",{type:'checkbox', id:'check_all'},[]);
|
||||
header0.appendChild(this.checkbox_all);
|
||||
this.checkbox_all.checked = false;
|
||||
this.checkbox_all.disabled = true;
|
||||
this.checkbox_all.addEventListener('change', function() { self.check_all.call(self, self.checkbox_all.checked); });
|
||||
|
||||
var header1 = document.createElement('th');
|
||||
header1.innerHTML = ' ID ';
|
||||
header1.style.width = "20px";
|
||||
var header2 = document.createElement('th');
|
||||
header2.innerHTML = 'Author';
|
||||
header2.style.width = "150px";
|
||||
var header3 = document.createElement('th');
|
||||
header3.innerHTML = 'Name';
|
||||
header3.style.width = "20%";
|
||||
var header4 = document.createElement('th');
|
||||
header4.innerHTML = 'Description';
|
||||
header4.style.width = "60%";
|
||||
// header4.classList.add('expandable-column');
|
||||
var header5 = document.createElement('th');
|
||||
header5.innerHTML = 'Install';
|
||||
header5.style.width = "130px";
|
||||
|
||||
header0.style.position = "sticky";
|
||||
header0.style.top = "0px";
|
||||
header1.style.position = "sticky";
|
||||
header1.style.top = "0px";
|
||||
header2.style.position = "sticky";
|
||||
header2.style.top = "0px";
|
||||
header3.style.position = "sticky";
|
||||
header3.style.top = "0px";
|
||||
header4.style.position = "sticky";
|
||||
header4.style.top = "0px";
|
||||
header5.style.position = "sticky";
|
||||
header5.style.top = "0px";
|
||||
|
||||
thead.appendChild(headerRow);
|
||||
headerRow.appendChild(header0);
|
||||
headerRow.appendChild(header1);
|
||||
headerRow.appendChild(header2);
|
||||
headerRow.appendChild(header3);
|
||||
headerRow.appendChild(header4);
|
||||
headerRow.appendChild(header5);
|
||||
|
||||
headerRow.style.backgroundColor = "Black";
|
||||
headerRow.style.color = "White";
|
||||
headerRow.style.textAlign = "center";
|
||||
headerRow.style.width = "100%";
|
||||
headerRow.style.padding = "0";
|
||||
|
||||
grid.appendChild(thead);
|
||||
grid.appendChild(tbody);
|
||||
|
||||
if(this.data)
|
||||
for (var i = 0; i < this.data.length; i++) {
|
||||
const data = this.data[i];
|
||||
let dataRow = document.createElement('tr');
|
||||
|
||||
let data0 = document.createElement('td');
|
||||
let checkbox = $el("input",{type:'checkbox', id:`check_${i}`},[]);
|
||||
data0.appendChild(checkbox);
|
||||
checkbox.checked = false;
|
||||
checkbox.addEventListener('change', function() { self.invalidate_checks.call(self, checkbox.checked, data.installed); });
|
||||
|
||||
var data1 = document.createElement('td');
|
||||
data1.style.textAlign = "center";
|
||||
data1.innerHTML = i+1;
|
||||
var data2 = document.createElement('td');
|
||||
data2.style.maxWidth = "100px";
|
||||
data2.className = "cm-node-author"
|
||||
data2.textContent = ` ${data.author}`;
|
||||
data2.style.whiteSpace = "nowrap";
|
||||
data2.style.overflow = "hidden";
|
||||
data2.style.textOverflow = "ellipsis";
|
||||
var data3 = document.createElement('td');
|
||||
data3.style.maxWidth = "200px";
|
||||
data3.style.wordWrap = "break-word";
|
||||
data3.className = "cm-node-name"
|
||||
data3.innerHTML = ` <a href=${data.reference} target="_blank"><font color="skyblue"><b>${data.title}</b></font></a>`;
|
||||
var data4 = document.createElement('td');
|
||||
data4.innerHTML = data.description;
|
||||
data4.className = "cm-node-desc"
|
||||
var data5 = document.createElement('td');
|
||||
data5.style.textAlign = "center";
|
||||
|
||||
var installBtn = document.createElement('button');
|
||||
installBtn.className = "cm-btn-install";
|
||||
var installBtn2 = null;
|
||||
var installBtn3 = null;
|
||||
|
||||
this.install_buttons.push(installBtn);
|
||||
|
||||
switch(data.installed) {
|
||||
case 'Disabled':
|
||||
installBtn3 = document.createElement('button');
|
||||
installBtn3.innerHTML = 'Enable';
|
||||
installBtn3.className = "cm-btn-enable";
|
||||
installBtn3.style.backgroundColor = 'blue';
|
||||
installBtn3.style.color = 'white';
|
||||
this.install_buttons.push(installBtn3);
|
||||
|
||||
installBtn.innerHTML = 'Uninstall';
|
||||
installBtn.style.backgroundColor = 'red';
|
||||
break;
|
||||
case 'Update':
|
||||
installBtn2 = document.createElement('button');
|
||||
installBtn2.innerHTML = 'Update';
|
||||
installBtn2.className = "cm-btn-update";
|
||||
installBtn2.style.backgroundColor = 'blue';
|
||||
installBtn2.style.color = 'white';
|
||||
this.install_buttons.push(installBtn2);
|
||||
|
||||
installBtn3 = document.createElement('button');
|
||||
installBtn3.innerHTML = 'Disable';
|
||||
installBtn3.className = "cm-btn-disable";
|
||||
installBtn3.style.backgroundColor = 'MediumSlateBlue';
|
||||
installBtn3.style.color = 'white';
|
||||
this.install_buttons.push(installBtn3);
|
||||
|
||||
installBtn.innerHTML = 'Uninstall';
|
||||
installBtn.style.backgroundColor = 'red';
|
||||
break;
|
||||
case 'True':
|
||||
installBtn3 = document.createElement('button');
|
||||
installBtn3.innerHTML = 'Disable';
|
||||
installBtn3.className = "cm-btn-disable";
|
||||
installBtn3.style.backgroundColor = 'MediumSlateBlue';
|
||||
installBtn3.style.color = 'white';
|
||||
this.install_buttons.push(installBtn3);
|
||||
|
||||
installBtn.innerHTML = 'Uninstall';
|
||||
installBtn.style.backgroundColor = 'red';
|
||||
break;
|
||||
case 'False':
|
||||
installBtn.innerHTML = 'Install';
|
||||
installBtn.style.backgroundColor = 'black';
|
||||
installBtn.style.color = 'white';
|
||||
break;
|
||||
default:
|
||||
installBtn.innerHTML = 'Try Install';
|
||||
installBtn.style.backgroundColor = 'Gray';
|
||||
installBtn.style.color = 'white';
|
||||
}
|
||||
|
||||
let j = i;
|
||||
if(installBtn2 != null) {
|
||||
installBtn2.style.width = "120px";
|
||||
installBtn2.addEventListener('click', function() {
|
||||
install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'update');
|
||||
});
|
||||
|
||||
data5.appendChild(installBtn2);
|
||||
}
|
||||
|
||||
if(installBtn3 != null) {
|
||||
installBtn3.style.width = "120px";
|
||||
installBtn3.addEventListener('click', function() {
|
||||
install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'toggle_active');
|
||||
});
|
||||
|
||||
data5.appendChild(installBtn3);
|
||||
}
|
||||
|
||||
installBtn.style.width = "120px";
|
||||
installBtn.addEventListener('click', function() {
|
||||
if(this.innerHTML == 'Uninstall') {
|
||||
if (confirm(`Are you sure uninstall ${data.title}?`)) {
|
||||
install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'uninstall');
|
||||
}
|
||||
}
|
||||
else {
|
||||
install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'install');
|
||||
}
|
||||
});
|
||||
|
||||
data5.appendChild(installBtn);
|
||||
|
||||
dataRow.style.backgroundColor = "var(--bg-color)";
|
||||
dataRow.style.color = "var(--fg-color)";
|
||||
dataRow.style.textAlign = "left";
|
||||
|
||||
dataRow.appendChild(data0);
|
||||
dataRow.appendChild(data1);
|
||||
dataRow.appendChild(data2);
|
||||
dataRow.appendChild(data3);
|
||||
dataRow.appendChild(data4);
|
||||
dataRow.appendChild(data5);
|
||||
tbody.appendChild(dataRow);
|
||||
|
||||
let buttons = [];
|
||||
if(installBtn) {
|
||||
buttons.push(installBtn);
|
||||
}
|
||||
if(installBtn2) {
|
||||
buttons.push(installBtn2);
|
||||
}
|
||||
if(installBtn3) {
|
||||
buttons.push(installBtn3);
|
||||
}
|
||||
|
||||
this.grid_rows[i] = {data:data, buttons:buttons, checkbox:checkbox, control:dataRow};
|
||||
}
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.style.width = "100%";
|
||||
panel.appendChild(grid);
|
||||
|
||||
function handleResize() {
|
||||
const parentHeight = self.element.clientHeight;
|
||||
const gridHeight = parentHeight - 200;
|
||||
|
||||
grid.style.height = gridHeight + "px";
|
||||
}
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
grid.style.position = "relative";
|
||||
grid.style.display = "inline-block";
|
||||
grid.style.width = "100%";
|
||||
grid.style.height = "100%";
|
||||
grid.style.overflowY = "scroll";
|
||||
this.element.style.height = "85%";
|
||||
this.element.style.width = "80%";
|
||||
this.element.appendChild(panel);
|
||||
|
||||
handleResize();
|
||||
}
|
||||
|
||||
createFilterCombo() {
|
||||
let combo = document.createElement("select");
|
||||
|
||||
combo.style.cssFloat = "left";
|
||||
combo.style.fontSize = "14px";
|
||||
combo.style.padding = "4px";
|
||||
combo.style.background = "black";
|
||||
combo.style.marginLeft = "2px";
|
||||
combo.style.width = "199px";
|
||||
combo.id = `combo-manger-filter`;
|
||||
combo.style.borderRadius = "15px";
|
||||
|
||||
let items =
|
||||
[
|
||||
{ value:'*', text:'Filter: all' },
|
||||
{ value:'Disabled', text:'Filter: disabled' },
|
||||
{ value:'Update', text:'Filter: update' },
|
||||
{ value:'True', text:'Filter: installed' },
|
||||
{ value:'False', text:'Filter: not-installed' },
|
||||
];
|
||||
|
||||
items.forEach(item => {
|
||||
const option = document.createElement("option");
|
||||
option.value = item.value;
|
||||
option.text = item.text;
|
||||
combo.appendChild(option);
|
||||
});
|
||||
|
||||
let self = this;
|
||||
combo.addEventListener('change', function(event) {
|
||||
self.filter = event.target.value;
|
||||
self.apply_searchbox();
|
||||
});
|
||||
|
||||
if(self.filter) {
|
||||
combo.value = self.filter;
|
||||
}
|
||||
|
||||
return combo;
|
||||
}
|
||||
|
||||
createHeaderControls() {
|
||||
let self = this;
|
||||
this.search_box = $el('input', {type:'text', id:'manager-customnode-search-box', placeholder:'input search keyword', value:this.search_keyword}, []);
|
||||
this.search_box.style.height = "25px";
|
||||
this.search_box.onkeydown = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
self.search_keyword = self.search_box.value;
|
||||
self.apply_searchbox();
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
self.search_keyword = self.search_box.value;
|
||||
self.apply_searchbox();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let search_button = document.createElement("button");
|
||||
search_button.innerHTML = "Search";
|
||||
search_button.onclick = () => {
|
||||
self.search_keyword = self.search_box.value;
|
||||
self.apply_searchbox();
|
||||
};
|
||||
search_button.style.display = "inline-block";
|
||||
|
||||
let filter_control = this.createFilterCombo();
|
||||
filter_control.style.display = "inline-block";
|
||||
|
||||
let cell = $el('td', {width:'100%'}, [filter_control, this.search_box, ' ', search_button]);
|
||||
let search_control = $el('table', {width:'100%'},
|
||||
[
|
||||
$el('tr', {}, [cell])
|
||||
]
|
||||
);
|
||||
|
||||
cell.style.textAlign = "right";
|
||||
|
||||
this.element.appendChild(search_control);
|
||||
}
|
||||
|
||||
async createBottomControls() {
|
||||
let close_button = document.createElement("button");
|
||||
close_button.innerHTML = "Close";
|
||||
close_button.onclick = () => { this.close(); }
|
||||
close_button.style.display = "inline-block";
|
||||
|
||||
this.message_box = $el('div', {id:'custom-installer-message'}, [$el('br'), '']);
|
||||
this.message_box.style.height = '60px';
|
||||
this.message_box.style.verticalAlign = 'middle';
|
||||
|
||||
this.element.appendChild(this.message_box);
|
||||
this.element.appendChild(close_button);
|
||||
}
|
||||
|
||||
async show(is_missing_node_mode) {
|
||||
this.is_missing_node_mode = is_missing_node_mode;
|
||||
try {
|
||||
this.invalidateControl();
|
||||
|
||||
this.element.style.display = "block";
|
||||
this.element.style.zIndex = 10001;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Failed to get custom node list. / ${exception}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
377
js/model-downloader.js
Normal file
377
js/model-downloader.js
Normal file
@ -0,0 +1,377 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { install_checked_custom_node, manager_instance } from "./common.js";
|
||||
|
||||
async function install_model(target) {
|
||||
if(ModelInstaller.instance) {
|
||||
ModelInstaller.instance.startInstall(target);
|
||||
|
||||
try {
|
||||
const response = await api.fetchApi('/model/install', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(target)
|
||||
});
|
||||
|
||||
const status = await response.json();
|
||||
app.ui.dialog.close();
|
||||
target.installed = 'True';
|
||||
return true;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Install failed: ${target.title} / ${exception}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
return false;
|
||||
}
|
||||
finally {
|
||||
await ModelInstaller.instance.invalidateControl();
|
||||
ModelInstaller.instance.updateMessage("<BR>To apply the installed model, please click the 'Refresh' button on the main menu.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getModelList() {
|
||||
var mode = "url";
|
||||
if(manager_instance.local_mode_checkbox.checked)
|
||||
mode = "local";
|
||||
|
||||
const response = await api.fetchApi(`/externalmodel/getlist?mode=${mode}`);
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export class ModelInstaller extends ComfyDialog {
|
||||
static instance = null;
|
||||
|
||||
install_buttons = [];
|
||||
message_box = null;
|
||||
data = null;
|
||||
|
||||
clear() {
|
||||
this.install_buttons = [];
|
||||
this.message_box = null;
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.search_keyword = '';
|
||||
this.element = $el("div.comfy-modal", { parent: document.body }, []);
|
||||
}
|
||||
|
||||
createControls() {
|
||||
return [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
onclick: () => { this.close(); }
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
startInstall(target) {
|
||||
const self = ModelInstaller.instance;
|
||||
|
||||
self.updateMessage(`<BR><font color="green">Installing '${target.name}'</font>`);
|
||||
|
||||
for(let i in self.install_buttons) {
|
||||
self.install_buttons[i].disabled = true;
|
||||
self.install_buttons[i].style.backgroundColor = 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
apply_searchbox(data) {
|
||||
let keyword = this.search_box.value.toLowerCase();
|
||||
for(let i in this.grid_rows) {
|
||||
let data = this.grid_rows[i].data;
|
||||
let content = data.name.toLowerCase() + data.type.toLowerCase() + data.base.toLowerCase() + data.description.toLowerCase();
|
||||
|
||||
if(this.filter && this.filter != '*') {
|
||||
if(this.filter != data.installed) {
|
||||
this.grid_rows[i].control.style.display = 'none';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(keyword == "")
|
||||
this.grid_rows[i].control.style.display = null;
|
||||
else if(content.includes(keyword)) {
|
||||
this.grid_rows[i].control.style.display = null;
|
||||
}
|
||||
else {
|
||||
this.grid_rows[i].control.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async invalidateControl() {
|
||||
this.clear();
|
||||
this.data = (await getModelList()).models;
|
||||
|
||||
while (this.element.children.length) {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
}
|
||||
|
||||
await this.createHeaderControls();
|
||||
|
||||
if(this.search_keyword) {
|
||||
this.search_box.value = this.search_keyword;
|
||||
}
|
||||
|
||||
await this.createGrid();
|
||||
await this.createBottomControls();
|
||||
|
||||
this.apply_searchbox(this.data);
|
||||
}
|
||||
|
||||
updateMessage(msg) {
|
||||
this.message_box.innerHTML = msg;
|
||||
}
|
||||
|
||||
async createGrid(models_json) {
|
||||
var grid = document.createElement('table');
|
||||
grid.setAttribute('id', 'external-models-grid');
|
||||
|
||||
var thead = document.createElement('thead');
|
||||
var tbody = document.createElement('tbody');
|
||||
|
||||
var headerRow = document.createElement('tr');
|
||||
thead.style.position = "sticky";
|
||||
thead.style.top = "0px";
|
||||
thead.style.borderCollapse = "collapse";
|
||||
thead.style.tableLayout = "fixed";
|
||||
|
||||
var header1 = document.createElement('th');
|
||||
header1.innerHTML = ' ID ';
|
||||
header1.style.width = "20px";
|
||||
var header2 = document.createElement('th');
|
||||
header2.innerHTML = 'Type';
|
||||
header2.style.width = "100px";
|
||||
var header3 = document.createElement('th');
|
||||
header3.innerHTML = 'Base';
|
||||
header3.style.width = "100px";
|
||||
var header4 = document.createElement('th');
|
||||
header4.innerHTML = 'Name';
|
||||
header4.style.width = "30%";
|
||||
var header5 = document.createElement('th');
|
||||
header5.innerHTML = 'Filename';
|
||||
header5.style.width = "20%";
|
||||
header5.style.tableLayout = "fixed";
|
||||
var header6 = document.createElement('th');
|
||||
header6.innerHTML = 'Description';
|
||||
header6.style.width = "50%";
|
||||
var header_down = document.createElement('th');
|
||||
header_down.innerHTML = 'Download';
|
||||
header_down.style.width = "50px";
|
||||
|
||||
thead.appendChild(headerRow);
|
||||
headerRow.appendChild(header1);
|
||||
headerRow.appendChild(header2);
|
||||
headerRow.appendChild(header3);
|
||||
headerRow.appendChild(header4);
|
||||
headerRow.appendChild(header5);
|
||||
headerRow.appendChild(header6);
|
||||
headerRow.appendChild(header_down);
|
||||
|
||||
headerRow.style.backgroundColor = "Black";
|
||||
headerRow.style.color = "White";
|
||||
headerRow.style.textAlign = "center";
|
||||
headerRow.style.width = "100%";
|
||||
headerRow.style.padding = "0";
|
||||
|
||||
grid.appendChild(thead);
|
||||
grid.appendChild(tbody);
|
||||
|
||||
this.grid_rows = {};
|
||||
|
||||
if(this.data)
|
||||
for (var i = 0; i < this.data.length; i++) {
|
||||
const data = this.data[i];
|
||||
var dataRow = document.createElement('tr');
|
||||
var data1 = document.createElement('td');
|
||||
data1.style.textAlign = "center";
|
||||
data1.innerHTML = i+1;
|
||||
var data2 = document.createElement('td');
|
||||
data2.innerHTML = ` ${data.type}`;
|
||||
var data3 = document.createElement('td');
|
||||
data3.innerHTML = ` ${data.base}`;
|
||||
var data4 = document.createElement('td');
|
||||
data4.className = "cm-node-name";
|
||||
data4.innerHTML = ` <a href=${data.reference} target="_blank"><font color="skyblue"><b>${data.name}</b></font></a>`;
|
||||
var data5 = document.createElement('td');
|
||||
data5.className = "cm-node-filename";
|
||||
data5.innerHTML = ` ${data.filename}`;
|
||||
data5.style.wordBreak = "break-all";
|
||||
var data6 = document.createElement('td');
|
||||
data6.className = "cm-node-desc";
|
||||
data6.innerHTML = data.description;
|
||||
data6.style.wordBreak = "break-all";
|
||||
var data_install = document.createElement('td');
|
||||
var installBtn = document.createElement('button');
|
||||
data_install.style.textAlign = "center";
|
||||
|
||||
installBtn.innerHTML = 'Install';
|
||||
this.install_buttons.push(installBtn);
|
||||
|
||||
switch(data.installed) {
|
||||
case 'True':
|
||||
installBtn.innerHTML = 'Installed';
|
||||
installBtn.style.backgroundColor = 'green';
|
||||
installBtn.style.color = 'white';
|
||||
installBtn.disabled = true;
|
||||
break;
|
||||
default:
|
||||
installBtn.innerHTML = 'Install';
|
||||
installBtn.style.backgroundColor = 'black';
|
||||
installBtn.style.color = 'white';
|
||||
break;
|
||||
}
|
||||
|
||||
installBtn.style.width = "100px";
|
||||
|
||||
installBtn.addEventListener('click', function() {
|
||||
install_model(data);
|
||||
});
|
||||
|
||||
data_install.appendChild(installBtn);
|
||||
|
||||
dataRow.style.backgroundColor = "var(--bg-color)";
|
||||
dataRow.style.color = "var(--fg-color)";
|
||||
dataRow.style.textAlign = "left";
|
||||
|
||||
dataRow.appendChild(data1);
|
||||
dataRow.appendChild(data2);
|
||||
dataRow.appendChild(data3);
|
||||
dataRow.appendChild(data4);
|
||||
dataRow.appendChild(data5);
|
||||
dataRow.appendChild(data6);
|
||||
dataRow.appendChild(data_install);
|
||||
tbody.appendChild(dataRow);
|
||||
|
||||
this.grid_rows[i] = {data:data, control:dataRow};
|
||||
}
|
||||
|
||||
let self = this;
|
||||
const panel = document.createElement('div');
|
||||
panel.style.width = "100%";
|
||||
panel.appendChild(grid);
|
||||
|
||||
function handleResize() {
|
||||
const parentHeight = self.element.clientHeight;
|
||||
const gridHeight = parentHeight - 200;
|
||||
|
||||
grid.style.height = gridHeight + "px";
|
||||
}
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
grid.style.position = "relative";
|
||||
grid.style.display = "inline-block";
|
||||
grid.style.width = "100%";
|
||||
grid.style.height = "100%";
|
||||
grid.style.overflowY = "scroll";
|
||||
this.element.style.height = "85%";
|
||||
this.element.style.width = "80%";
|
||||
this.element.appendChild(panel);
|
||||
|
||||
handleResize();
|
||||
}
|
||||
|
||||
createFilterCombo() {
|
||||
let combo = document.createElement("select");
|
||||
|
||||
combo.style.cssFloat = "left";
|
||||
combo.style.fontSize = "14px";
|
||||
combo.style.padding = "4px";
|
||||
combo.style.background = "black";
|
||||
combo.style.marginLeft = "2px";
|
||||
combo.style.width = "199px";
|
||||
combo.id = `combo-manger-filter`;
|
||||
combo.style.borderRadius = "15px";
|
||||
|
||||
let items =
|
||||
[
|
||||
{ value:'*', text:'Filter: all' },
|
||||
{ value:'True', text:'Filter: installed' },
|
||||
{ value:'False', text:'Filter: not-installed' },
|
||||
];
|
||||
|
||||
items.forEach(item => {
|
||||
const option = document.createElement("option");
|
||||
option.value = item.value;
|
||||
option.text = item.text;
|
||||
combo.appendChild(option);
|
||||
});
|
||||
|
||||
let self = this;
|
||||
combo.addEventListener('change', function(event) {
|
||||
self.filter = event.target.value;
|
||||
self.apply_searchbox();
|
||||
});
|
||||
|
||||
return combo;
|
||||
}
|
||||
|
||||
createHeaderControls() {
|
||||
let self = this;
|
||||
this.search_box = $el('input', {type:'text', id:'manager-model-search-box', placeholder:'input search keyword', value:this.search_keyword}, []);
|
||||
this.search_box.style.height = "25px";
|
||||
this.search_box.onkeydown = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
self.search_keyword = self.search_box.value;
|
||||
self.apply_searchbox();
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
self.search_keyword = self.search_box.value;
|
||||
self.apply_searchbox();
|
||||
}
|
||||
};
|
||||
|
||||
let search_button = document.createElement("button");
|
||||
search_button.innerHTML = "Search";
|
||||
search_button.onclick = () => {
|
||||
self.search_keyword = self.search_box.value;
|
||||
self.apply_searchbox();
|
||||
};
|
||||
search_button.style.display = "inline-block";
|
||||
|
||||
let filter_control = this.createFilterCombo();
|
||||
filter_control.style.display = "inline-block";
|
||||
|
||||
let cell = $el('td', {width:'100%'}, [filter_control, this.search_box, ' ', search_button]);
|
||||
let search_control = $el('table', {width:'100%'},
|
||||
[
|
||||
$el('tr', {}, [cell])
|
||||
]
|
||||
);
|
||||
|
||||
cell.style.textAlign = "right";
|
||||
this.element.appendChild(search_control);
|
||||
}
|
||||
|
||||
async createBottomControls() {
|
||||
var close_button = document.createElement("button");
|
||||
close_button.innerHTML = "Close";
|
||||
close_button.onclick = () => { this.close(); }
|
||||
close_button.style.display = "inline-block";
|
||||
|
||||
this.message_box = $el('div', {id:'custom-download-message'}, [$el('br'), '']);
|
||||
this.message_box.style.height = '60px';
|
||||
this.message_box.style.verticalAlign = 'middle';
|
||||
|
||||
this.element.appendChild(this.message_box);
|
||||
this.element.appendChild(close_button);
|
||||
}
|
||||
|
||||
async show() {
|
||||
try {
|
||||
this.invalidateControl();
|
||||
this.element.style.display = "block";
|
||||
this.element.style.zIndex = 10001;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Failed to get external model list. / ${exception}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
279
js/snapshot.js
Normal file
279
js/snapshot.js
Normal file
@ -0,0 +1,279 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { manager_instance } from "./common.js";
|
||||
|
||||
|
||||
async function restore_snapshot(target) {
|
||||
if(SnapshotManager.instance) {
|
||||
try {
|
||||
const response = await api.fetchApi(`/snapshot/restore?target=${target}`, { cache: "no-store" });
|
||||
if(response.status == 400) {
|
||||
app.ui.dialog.show(`Restore snapshot failed: ${target.title} / ${exception}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
}
|
||||
|
||||
app.ui.dialog.close();
|
||||
return true;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Restore snapshot failed: ${target.title} / ${exception}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
return false;
|
||||
}
|
||||
finally {
|
||||
await SnapshotManager.instance.invalidateControl();
|
||||
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please restart ComfyUI.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function remove_snapshot(target) {
|
||||
if(SnapshotManager.instance) {
|
||||
try {
|
||||
const response = await api.fetchApi(`/snapshot/remove?target=${target}`, { cache: "no-store" });
|
||||
if(response.status == 400) {
|
||||
app.ui.dialog.show(`Remove snapshot failed: ${target.title} / ${exception}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
}
|
||||
|
||||
app.ui.dialog.close();
|
||||
return true;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Restore snapshot failed: ${target.title} / ${exception}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
return false;
|
||||
}
|
||||
finally {
|
||||
await SnapshotManager.instance.invalidateControl();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function save_current_snapshot() {
|
||||
try {
|
||||
const response = await api.fetchApi('/snapshot/save', { cache: "no-store" });
|
||||
app.ui.dialog.close();
|
||||
return true;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Backup snapshot failed: ${exception}`);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
return false;
|
||||
}
|
||||
finally {
|
||||
await SnapshotManager.instance.invalidateControl();
|
||||
SnapshotManager.instance.updateMessage("<BR>Current snapshot saved.");
|
||||
}
|
||||
}
|
||||
|
||||
async function getSnapshotList() {
|
||||
const response = await api.fetchApi(`/snapshot/getlist`);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export class SnapshotManager extends ComfyDialog {
|
||||
static instance = null;
|
||||
|
||||
restore_buttons = [];
|
||||
message_box = null;
|
||||
data = null;
|
||||
|
||||
clear() {
|
||||
this.restore_buttons = [];
|
||||
this.message_box = null;
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.search_keyword = '';
|
||||
this.element = $el("div.comfy-modal", { parent: document.body }, []);
|
||||
}
|
||||
|
||||
async remove_item() {
|
||||
caller.disableButtons();
|
||||
|
||||
await caller.invalidateControl();
|
||||
}
|
||||
|
||||
createControls() {
|
||||
return [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
onclick: () => { this.close(); }
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
startRestore(target) {
|
||||
const self = SnapshotManager.instance;
|
||||
|
||||
self.updateMessage(`<BR><font color="green">Restore snapshot '${target.name}'</font>`);
|
||||
|
||||
for(let i in self.restore_buttons) {
|
||||
self.restore_buttons[i].disabled = true;
|
||||
self.restore_buttons[i].style.backgroundColor = 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
async invalidateControl() {
|
||||
this.clear();
|
||||
this.data = (await getSnapshotList()).items;
|
||||
|
||||
while (this.element.children.length) {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
}
|
||||
|
||||
await this.createGrid();
|
||||
await this.createBottomControls();
|
||||
}
|
||||
|
||||
updateMessage(msg) {
|
||||
this.message_box.innerHTML = msg;
|
||||
}
|
||||
|
||||
async createGrid(models_json) {
|
||||
var grid = document.createElement('table');
|
||||
grid.setAttribute('id', 'snapshot-list-grid');
|
||||
|
||||
var thead = document.createElement('thead');
|
||||
var tbody = document.createElement('tbody');
|
||||
|
||||
var headerRow = document.createElement('tr');
|
||||
thead.style.position = "sticky";
|
||||
thead.style.top = "0px";
|
||||
thead.style.borderCollapse = "collapse";
|
||||
thead.style.tableLayout = "fixed";
|
||||
|
||||
var header1 = document.createElement('th');
|
||||
header1.innerHTML = ' ID ';
|
||||
header1.style.width = "20px";
|
||||
var header2 = document.createElement('th');
|
||||
header2.innerHTML = 'Datetime';
|
||||
header2.style.width = "100%";
|
||||
var header_button = document.createElement('th');
|
||||
header_button.innerHTML = 'Action';
|
||||
header_button.style.width = "100px";
|
||||
|
||||
thead.appendChild(headerRow);
|
||||
headerRow.appendChild(header1);
|
||||
headerRow.appendChild(header2);
|
||||
headerRow.appendChild(header_button);
|
||||
|
||||
headerRow.style.backgroundColor = "Black";
|
||||
headerRow.style.color = "White";
|
||||
headerRow.style.textAlign = "center";
|
||||
headerRow.style.width = "100%";
|
||||
headerRow.style.padding = "0";
|
||||
|
||||
grid.appendChild(thead);
|
||||
grid.appendChild(tbody);
|
||||
|
||||
this.grid_rows = {};
|
||||
|
||||
if(this.data)
|
||||
for (var i = 0; i < this.data.length; i++) {
|
||||
const data = this.data[i];
|
||||
var dataRow = document.createElement('tr');
|
||||
var data1 = document.createElement('td');
|
||||
data1.style.textAlign = "center";
|
||||
data1.innerHTML = i+1;
|
||||
var data2 = document.createElement('td');
|
||||
data2.innerHTML = ` ${data}`;
|
||||
var data_button = document.createElement('td');
|
||||
data_button.style.textAlign = "center";
|
||||
|
||||
var restoreBtn = document.createElement('button');
|
||||
restoreBtn.innerHTML = 'Restore';
|
||||
restoreBtn.style.width = "100px";
|
||||
restoreBtn.style.backgroundColor = 'blue';
|
||||
|
||||
restoreBtn.addEventListener('click', function() {
|
||||
restore_snapshot(data);
|
||||
});
|
||||
|
||||
var removeBtn = document.createElement('button');
|
||||
removeBtn.innerHTML = 'Remove';
|
||||
removeBtn.style.width = "100px";
|
||||
removeBtn.style.backgroundColor = 'red';
|
||||
|
||||
removeBtn.addEventListener('click', function() {
|
||||
remove_snapshot(data);
|
||||
});
|
||||
|
||||
data_button.appendChild(restoreBtn);
|
||||
data_button.appendChild(removeBtn);
|
||||
|
||||
dataRow.style.backgroundColor = "var(--bg-color)";
|
||||
dataRow.style.color = "var(--fg-color)";
|
||||
dataRow.style.textAlign = "left";
|
||||
|
||||
dataRow.appendChild(data1);
|
||||
dataRow.appendChild(data2);
|
||||
dataRow.appendChild(data_button);
|
||||
tbody.appendChild(dataRow);
|
||||
|
||||
this.grid_rows[i] = {data:data, control:dataRow};
|
||||
}
|
||||
|
||||
let self = this;
|
||||
const panel = document.createElement('div');
|
||||
panel.style.width = "100%";
|
||||
panel.appendChild(grid);
|
||||
|
||||
function handleResize() {
|
||||
const parentHeight = self.element.clientHeight;
|
||||
const gridHeight = parentHeight - 200;
|
||||
|
||||
grid.style.height = gridHeight + "px";
|
||||
}
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
grid.style.position = "relative";
|
||||
grid.style.display = "inline-block";
|
||||
grid.style.width = "100%";
|
||||
grid.style.height = "100%";
|
||||
grid.style.overflowY = "scroll";
|
||||
this.element.style.height = "85%";
|
||||
this.element.style.width = "80%";
|
||||
this.element.appendChild(panel);
|
||||
|
||||
handleResize();
|
||||
}
|
||||
|
||||
async createBottomControls() {
|
||||
var close_button = document.createElement("button");
|
||||
close_button.innerHTML = "Close";
|
||||
close_button.onclick = () => { this.close(); }
|
||||
close_button.style.display = "inline-block";
|
||||
|
||||
var save_button = document.createElement("button");
|
||||
save_button.innerHTML = "Save snapshot";
|
||||
save_button.onclick = () => { save_current_snapshot(); }
|
||||
save_button.style.display = "inline-block";
|
||||
save_button.style.horizontalAlign = "right";
|
||||
|
||||
this.message_box = $el('div', {id:'custom-download-message'}, [$el('br'), '']);
|
||||
this.message_box.style.height = '60px';
|
||||
this.message_box.style.verticalAlign = 'middle';
|
||||
|
||||
this.element.appendChild(this.message_box);
|
||||
this.element.appendChild(close_button);
|
||||
this.element.appendChild(save_button);
|
||||
}
|
||||
|
||||
async show() {
|
||||
try {
|
||||
this.invalidateControl();
|
||||
this.element.style.display = "block";
|
||||
this.element.style.zIndex = 10001;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Failed to get external model list. / ${exception}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
misc/menu.jpg
BIN
misc/menu.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 68 KiB |
BIN
misc/snapshot.jpg
Normal file
BIN
misc/snapshot.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
@ -1,14 +1,84 @@
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"author": "shadowcz007",
|
||||
"title": "comfyui-mixlab-nodes",
|
||||
"reference": "https://github.com/shadowcz007/comfyui-mixlab-nodes",
|
||||
"author": "Jordach",
|
||||
"title": "comfy-consistency-vae",
|
||||
"reference": "https://github.com/Jordach/comfy-consistency-vae",
|
||||
"files": [
|
||||
"https://github.com/shadowcz007/comfyui-mixlab-nodes"
|
||||
"https://github.com/Jordach/comfy-consistency-vae"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:RandomPrompt"
|
||||
"description": "Nodes: Comfy_ConsistencyVAE"
|
||||
},
|
||||
{
|
||||
"author": "gameltb",
|
||||
"title": "ComfyUI_stable_fast",
|
||||
"reference": "https://github.com/gameltb/ComfyUI_stable_fast",
|
||||
"files": [
|
||||
"https://github.com/gameltb/ComfyUI_stable_fast"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:ApplyStableFastUnet. Experimental usage of stable-fast."
|
||||
},
|
||||
{
|
||||
"author": "jn-jairo",
|
||||
"title": "jn_node_suite_comfyui [WIP]",
|
||||
"reference": "https://github.com/jn-jairo/jn_node_suite_comfyui",
|
||||
"files": [
|
||||
"https://github.com/jn-jairo/jn_node_suite_comfyui"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Image manipulation nodes, Temperature control nodes, Tiling nodes, Primitive and operation nodes, ..."
|
||||
},
|
||||
{
|
||||
"author": "PluMaZero",
|
||||
"title": "ComfyUI-SpaceFlower",
|
||||
"reference": "https://github.com/PluMaZero/ComfyUI-SpaceFlower",
|
||||
"files": [
|
||||
"https://github.com/PluMaZero/ComfyUI-SpaceFlower"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: SpaceFlower_Prompt."
|
||||
},
|
||||
{
|
||||
"author": "laksjdjf",
|
||||
"title": "ssd-1b-comfyui",
|
||||
"reference": "https://github.com/laksjdjf/ssd-1b-comfyui",
|
||||
"files": [
|
||||
"https://github.com/laksjdjf/ssd-1b-comfyui"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Experimental node for SSD-1B. This node is not need for latest comfyui."
|
||||
},
|
||||
{
|
||||
"author": "Feidorian",
|
||||
"title": "feidorian-nodes",
|
||||
"reference": "https://github.com/Feidorian/feidorian-ComfyNodes",
|
||||
"files": [
|
||||
"https://github.com/Feidorian/feidorian-ComfyNodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Feidorian_WorkflowImageLoader"
|
||||
},
|
||||
{
|
||||
"author": "flowtyone",
|
||||
"title": "comfyui-flowty-lcm",
|
||||
"reference": "https://github.com/flowtyone/comfyui-flowty-lcm",
|
||||
"files": [
|
||||
"https://github.com/flowtyone/comfyui-flowty-lcm"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This is a comfyui early testing node for LCM, adapted from <a href='https://github.com/0xbitches/sd-webui-lcm'/>https://github.com/0xbitches/sd-webui-lcm</a>. It uses the diffusers backend unfortunately and not comfy's model loading mechanism. But the intention here is just to be able to execute lcm inside comfy.<BR>NOTE: 0xbitches's 'Latent Consistency Model for ComfyUI' is original implementation."
|
||||
},
|
||||
{
|
||||
"author": "doucx",
|
||||
"title": "ComfyUI_WcpD_Utility_Kit",
|
||||
"reference": "https://github.com/doucx/ComfyUI_WcpD_Utility_Kit",
|
||||
"files": [
|
||||
"https://github.com/doucx/ComfyUI_WcpD_Utility_Kit"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: MergeStrings, ExecStrAsCode, RandnLatentImage. <p style='background-color: black; color: red;'>NOTE: This extension includes the ability to execute code as a string in nodes. Be cautious during installation, as it can pose a security risk.</p>"
|
||||
},
|
||||
{
|
||||
"author": "AbyssYuan0",
|
||||
@ -18,7 +88,7 @@
|
||||
"https://github.com/AbyssYuan0/ComfyUI_BadgerTools"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodees: ImageOverlap-badger, FloatToInt-badger, IntToString-badger, FloatToString-badger."
|
||||
"description": "Nodes: ImageOverlap-badger, FloatToInt-badger, IntToString-badger, FloatToString-badger."
|
||||
},
|
||||
{
|
||||
"author": "WSJUSA",
|
||||
|
||||
@ -1,5 +1,25 @@
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"author": "LucianoCirino",
|
||||
"title": "Efficiency Nodes for ComfyUI [LEGACY]",
|
||||
"reference": "https://github.com/LucianoCirino/efficiency-nodes-comfyui",
|
||||
"files": [
|
||||
"https://github.com/LucianoCirino/efficiency-nodes-comfyui"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A collection of ComfyUI custom nodes to help streamline workflows and reduce total node count.<BR>NOTE: This repository is the original repository but is no longer maintained. Please use the forked version by jags."
|
||||
},
|
||||
{
|
||||
"author": "GeLi1989",
|
||||
"title": "roop nodes for ComfyUI",
|
||||
"reference": "https://github.com/GeLi1989/GK-beifen-ComfyUI_roop",
|
||||
"files": [
|
||||
"https://github.com/GeLi1989/GK-beifen-ComfyUI_roop"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI nodes for the roop A1111 webui script. NOTE: Need to download <a href='https://github.com/SumDeusVitae/FaceSwap_v01/raw/main/inswapper_128.onnx' target='blank'/>model</a> to use this node. NOTE: This is removed."
|
||||
},
|
||||
{
|
||||
"author": "ProDALOR",
|
||||
"title": "comfyui_u2net",
|
||||
|
||||
@ -1,5 +1,286 @@
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"author": "nagolinc",
|
||||
"title": "ComfyUI_FastVAEDecorder_SDXL",
|
||||
"reference": "https://github.com/nagolinc/ComfyUI_FastVAEDecorder_SDXL",
|
||||
"files": [
|
||||
"https://github.com/nagolinc/ComfyUI_FastVAEDecorder_SDXL"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Based off of: <a href='https://github.com/Birch-san/diffusers-play/tree/main/approx_vae'>Birch-san/diffusers-play/approx_vae</a>. This ComfyUI node allows you to quickly preview SDXL 1.0 latents."
|
||||
},
|
||||
{
|
||||
"author": "jags111",
|
||||
"title": "ComfyUI_Jags_VectorMagic",
|
||||
"reference": "https://github.com/jags111/ComfyUI_Jags_VectorMagic",
|
||||
"files": [
|
||||
"https://github.com/jags111/ComfyUI_Jags_VectorMagic"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "a collection of nodes to explore Vector and image manipulation"
|
||||
},
|
||||
{
|
||||
"author": "Trung0246",
|
||||
"title": "ComfyUI-0246",
|
||||
"reference": "https://github.com/Trung0246/ComfyUI-0246",
|
||||
"files": [
|
||||
"https://github.com/Trung0246/ComfyUI-0246"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Highway. Random nodes for ComfyUI I made to solve my struggle with ComfyUI. Have varying quality."
|
||||
},
|
||||
{
|
||||
"author": "PCMonsterx",
|
||||
"title": "ComfyUI-CSV-Loader",
|
||||
"reference": "https://github.com/PCMonsterx/ComfyUI-CSV-Loader",
|
||||
"files": [
|
||||
"https://github.com/PCMonsterx/ComfyUI-CSV-Loader"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "CSV Loader for prompt building within ComfyUI interface. Allows access to positive/negative prompts associated with a name. Selections are being pulled from CSV files."
|
||||
},
|
||||
{
|
||||
"author": "IAmMatan.com",
|
||||
"title": "ComfyUI Serving toolkit",
|
||||
"reference": "https://github.com/matan1905/ComfyUI-Serving-Toolkit",
|
||||
"files": [
|
||||
"https://github.com/matan1905/ComfyUI-Serving-Toolkit"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension adds nodes that allow you to easily serve your workflow (for example using a discord bot) "
|
||||
},
|
||||
{
|
||||
"author": "ParmanBabra",
|
||||
"title": "ComfyUI-Malefish-Custom-Scripts",
|
||||
"reference": "https://github.com/ParmanBabra/ComfyUI-Malefish-Custom-Scripts",
|
||||
"files": [
|
||||
"https://github.com/ParmanBabra/ComfyUI-Malefish-Custom-Scripts"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Multi Lora Loader, Random (Prompt), Combine (Prompt), CSV Prompts Loader"
|
||||
},
|
||||
{
|
||||
"author": "mikkel",
|
||||
"title": "ComfyUI - Mask Bounding Box",
|
||||
"reference": "https://github.com/mikkel/comfyui-mask-boundingbox",
|
||||
"files": [
|
||||
"https://github.com/mikkel/comfyui-mask-boundingbox"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "The ComfyUI Mask Bounding Box Plugin provides functionalities for selecting a specific size mask from an image. Can be combined with ClipSEG to replace any aspect of an SDXL image with an SD1.5 output."
|
||||
},
|
||||
{
|
||||
"author": "THtianhao",
|
||||
"title": "ComfyUI-FaceChain",
|
||||
"reference": "https://github.com/THtianhao/ComfyUI-FaceChain",
|
||||
"files": [
|
||||
"https://github.com/THtianhao/ComfyUI-FaceChain"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:FC_LoraMerge."
|
||||
},
|
||||
{
|
||||
"author": "noEmbryo",
|
||||
"title": "ComfyUI-noEmbryo nodes",
|
||||
"reference": "https://github.com/noembryo/ComfyUI-noEmbryo",
|
||||
"files": [
|
||||
"https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/nodes.py"
|
||||
],
|
||||
"install_type": "copy",
|
||||
"description": "PromptTermList (1-6): are some nodes that help with the creation of Prompts inside ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "aianimation55",
|
||||
"title": "Comfy UI FatLabels",
|
||||
"reference": "https://github.com/aianimation55/ComfyUI-FatLabels",
|
||||
"files": [
|
||||
"https://github.com/aianimation55/ComfyUI-FatLabels"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "It's a super simple custom node for Comfy UI, to generate text, with a font size option. Useful for bigger labelling of nodes, helpful for wider screen captures or tutorials. Plus you can of course use the text within your generations."
|
||||
},
|
||||
{
|
||||
"author": "idrirap",
|
||||
"title": "ComfyUI-Lora-Auto-Trigger-Words",
|
||||
"reference": "https://github.com/idrirap/ComfyUI-Lora-Auto-Trigger-Words",
|
||||
"files": [
|
||||
"https://github.com/idrirap/ComfyUI-Lora-Auto-Trigger-Words"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This project is a fork of <a href='https://github.com/Extraltodeus/LoadLoraWithTags'>https://github.com/Extraltodeus/LoadLoraWithTags</a> The aim of these custom nodes is to get an easy access to the tags used to trigger a lora."
|
||||
},
|
||||
{
|
||||
"author": "jags111",
|
||||
"title": "Efficiency Nodes for ComfyUI Version 2.0+",
|
||||
"reference": "https://github.com/jags111/efficiency-nodes-comfyui",
|
||||
"files": [
|
||||
"https://github.com/jags111/efficiency-nodes-comfyui"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A collection of ComfyUI custom nodes to help streamline workflows and reduce total node count.<p style='background-color: black; color: red;'>NOTE: This node is originally created by LucianoCirino, but the <a href='https://github.com/LucianoCirino/efficiency-nodes-comfyui'>original repository</a> is no longer maintained and has been forked by a new maintainer. To use the forked version, you should uninstall the original version and <B>REINSTALL</B> this one.</p>"
|
||||
},
|
||||
{
|
||||
"author": "M1kep",
|
||||
"title": "ComfyUI-OtherVAEs",
|
||||
"reference": "https://github.com/M1kep/ComfyUI-OtherVAEs",
|
||||
"files": [
|
||||
"https://github.com/M1kep/ComfyUI-OtherVAEs"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: TAESD VAE Decode"
|
||||
},
|
||||
{
|
||||
"author": "Fictiverse",
|
||||
"title": "ComfyUI Fictiverse Nodes",
|
||||
"reference": "https://github.com/Fictiverse/ComfyUI_Fictiverse",
|
||||
"files": [
|
||||
"https://github.com/Fictiverse/ComfyUI_Fictiverse"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Color correction."
|
||||
},
|
||||
{
|
||||
"author": "kinfolk0117",
|
||||
"title": "ComfyUI_SimpleTiles",
|
||||
"reference": "https://github.com/kinfolk0117/ComfyUI_SimpleTiles",
|
||||
"files": [
|
||||
"https://github.com/kinfolk0117/ComfyUI_SimpleTiles"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:TileSplit, TileMerge."
|
||||
},
|
||||
{
|
||||
"author": "CaptainGrock",
|
||||
"title": "ComfyUIInvisibleWatermark",
|
||||
"reference": "https://github.com/CaptainGrock/ComfyUIInvisibleWatermark",
|
||||
"files": [
|
||||
"https://github.com/CaptainGrock/ComfyUIInvisibleWatermark/raw/main/Invisible%20Watermark.py"
|
||||
],
|
||||
"install_type": "copy",
|
||||
"description": "Nodes:Apply Invisible Watermark, Extract Watermark. Adds up to 12 characters encoded into an image that can be extracted."
|
||||
},
|
||||
{
|
||||
"author": "apesplat",
|
||||
"title": "ezXY scripts and nodes",
|
||||
"reference": "https://github.com/GMapeSplat/ComfyUI_ezXY",
|
||||
"files": [
|
||||
"https://github.com/GMapeSplat/ComfyUI_ezXY"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Extensions/Patches: Enables linking float and integer inputs and ouputs. Values are automatically cast to the correct type and clamped to the correct range. Works with both builtin and custom nodes.<p style='background-color: black; color: red;'>NOTE: This repo patches ComfyUI's validate_inputs and map_node_over_list functions while running. May break depending on your version of ComfyUI. Can be deactivated in config.yaml.</p>Nodes: A collection of nodes for facilitating the generation of XY plots. Capable of plotting changes over most primitive values."
|
||||
},
|
||||
{
|
||||
"author": "noxinias",
|
||||
"title": "ComfyUI_NoxinNodes",
|
||||
"reference": "https://github.com/noxinias/ComfyUI_NoxinNodes",
|
||||
"files": [
|
||||
"https://github.com/noxinias/ComfyUI_NoxinNodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Noxin Complete Chime, Noxin Scaled Resolutions, Load from Noxin Prompt Library, Save to Noxin Prompt Library"
|
||||
},
|
||||
{
|
||||
"author": "taabata",
|
||||
"title": "LCM_Inpaint-Outpaint_Comfy",
|
||||
"reference": "https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy",
|
||||
"files": [
|
||||
"https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom nodes for inpainting/outpainting using the new latent consistency model (LCM)"
|
||||
},
|
||||
{
|
||||
"author": "0xbitches",
|
||||
"title": "Latent Consistency Model for ComfyUI",
|
||||
"reference": "https://github.com/0xbitches/ComfyUI-LCM",
|
||||
"files": [
|
||||
"https://github.com/0xbitches/ComfyUI-LCM"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This custom node implements a Latent Consistency Model sampler in ComfyUI. (LCM)"
|
||||
},
|
||||
{
|
||||
"author": "flowtyone",
|
||||
"title": "comfyui-flowty-lcm",
|
||||
"reference": "https://github.com/flowtyone/comfyui-flowty-lcm",
|
||||
"files": [
|
||||
"https://github.com/flowtyone/comfyui-flowty-lcm"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This is a comfyui early testing node for LCM, adapted from <a href='https://github.com/0xbitches/sd-webui-lcm'/>https://github.com/0xbitches/sd-webui-lcm</a>. It uses the diffusers backend unfortunately and not comfy's model loading mechanism. But the intention here is just to be able to execute lcm inside comfy.<BR>NOTE: 0xbitches's 'Latent Consistency Model for ComfyUI' is original implementation."
|
||||
},
|
||||
{
|
||||
"author": "aszc-dev",
|
||||
"title": "Core ML Suite for ComfyUI",
|
||||
"reference": "https://github.com/aszc-dev/ComfyUI-CoreMLSuite",
|
||||
"files": [
|
||||
"https://github.com/aszc-dev/ComfyUI-CoreMLSuite"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension contains a set of custom nodes for ComfyUI that allow you to use Core ML models in your ComfyUI workflows. The models can be obtained here, or you can convert your own models using coremltools. The main motivation behind using Core ML models in ComfyUI is to allow you to utilize the ANE (Apple Neural Engine) on Apple Silicon (M1/M2) machines to improve performance."
|
||||
},
|
||||
{
|
||||
"author": "Fannovel16",
|
||||
"title": "ComfyUI MotionDiff",
|
||||
"reference": "https://github.com/Fannovel16/ComfyUI-MotionDiff",
|
||||
"files": [
|
||||
"https://github.com/Fannovel16/ComfyUI-MotionDiff"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Implementation of MDM, MotionDiffuse and ReMoDiffuse into ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "ostris",
|
||||
"title": "Ostris Nodes ComfyUI",
|
||||
"reference": "https://github.com/ostris/ostris_nodes_comfyui",
|
||||
"files": [
|
||||
"https://github.com/ostris/ostris_nodes_comfyui"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"nodename_pattern": "- Ostris$",
|
||||
"description": "This is a collection of custom nodes for ComfyUI that I made for some QOL. I will be adding much more advanced ones in the future once I get more familiar with the API."
|
||||
},
|
||||
{
|
||||
"author": "city96",
|
||||
"title": "Extra Models for ComfyUI",
|
||||
"reference": "https://github.com/city96/ComfyUI_ExtraModels",
|
||||
"files": [
|
||||
"https://github.com/city96/ComfyUI_ExtraModels"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension aims to add support for various random image diffusion models to ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "shadowcz007",
|
||||
"title": "comfyui-mixlab-nodes [WIP]",
|
||||
"reference": "https://github.com/shadowcz007/comfyui-mixlab-nodes",
|
||||
"files": [
|
||||
"https://github.com/shadowcz007/comfyui-mixlab-nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: RandomPrompt, TransparentImage, LoadImageFromPath, SplitLongMask, ImagesCrop, RunWorkflow"
|
||||
},
|
||||
{
|
||||
"author": "chrisgoringe",
|
||||
"title": "Prompt Info",
|
||||
"reference": "https://github.com/chrisgoringe/cg-prompt-info",
|
||||
"files": [
|
||||
"https://github.com/chrisgoringe/cg-prompt-info"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Prompt Info"
|
||||
},
|
||||
{
|
||||
"author": "drustan-hawk",
|
||||
"title": "primitive-types",
|
||||
"reference": "https://github.com/drustan-hawk/primitive-types",
|
||||
"files": [
|
||||
"https://github.com/drustan-hawk/primitive-types"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A text-to-speech plugin used under ComfyUI. It utilizes the Microsoft Speech TTS interface to convert text content into MP3 format audio files."
|
||||
},
|
||||
{
|
||||
"author": "chflame163",
|
||||
"title": "ComfyUI_MSSpeech_TTS",
|
||||
@ -58,7 +339,7 @@
|
||||
"https://github.com/chibiace/ComfyUI-Chibi-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Loader, Prompts, ..."
|
||||
"description": "Nodes:Loader, Prompts, ImageTool, Wildcards, LoadEmbedding, ConditionText, SaveImages, ..."
|
||||
},
|
||||
{
|
||||
"author": "YMC",
|
||||
@ -102,13 +383,13 @@
|
||||
},
|
||||
{
|
||||
"author": "chrish-slingshot",
|
||||
"title": "ComfyUI-ImageGlitcher",
|
||||
"reference": "https://github.com/chrish-slingshot/ComfyUI-ImageGlitcher",
|
||||
"title": "CrasH Utils",
|
||||
"reference": "https://github.com/chrish-slingshot/CrasHUtils",
|
||||
"files": [
|
||||
"https://github.com/chrish-slingshot/ComfyUI-ImageGlitcher"
|
||||
"https://github.com/chrish-slingshot/CrasHUtils"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: ImageGlitcher. Based on the HTML image glitcher by Felix Turner <a href='https://www.airtightinteractive.com/demos/js/imageglitcher/' target='blank'>here</a>."
|
||||
"description": "A mixture of effects and quality of life nodes. Nodes: ImageGlitcher (gives an image a cool glitchy effect), ColorStylizer (highlights a single color in an image), QueryLocalLLM (queries a local LLM API though oobabooga), SDXLReslution (resolution picker for the standard SDXL resolutions, the complete list), SDXLResolutionSplit (splits the SDXL resolution into width and height). "
|
||||
},
|
||||
{
|
||||
"author": "whatbirdisthat",
|
||||
@ -128,7 +409,7 @@
|
||||
"https://github.com/a1lazydog/ComfyUI-AudioScheduler"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: LoadAudioFromPath, AudioToFFTs, BatchAmplitudeSchedule, AmplitudeSchedule."
|
||||
"description": "Load mp3 files and use the audio nodes to power animations and prompt scheduling. Use with FizzNodes."
|
||||
},
|
||||
{
|
||||
"author": "storyicon",
|
||||
@ -277,7 +558,6 @@
|
||||
"files": [
|
||||
"https://github.com/kijai/ComfyUI-KJNodes"
|
||||
],
|
||||
"pip": ["librosa"],
|
||||
"install_type": "git-clone",
|
||||
"description": "Various quality of life -nodes for ComfyUI, mostly just visual stuff to improve usability."
|
||||
},
|
||||
@ -521,506 +801,6 @@
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Add Image Save nodes for TIFF 16 bit and EXR 32 bit formats. Probably only useful if you're applying a LUT or other color corrections, and care about preserving as much color accuracy as possible."
|
||||
},
|
||||
{
|
||||
"author": "meap158",
|
||||
"title": "ComfyUI Prompt Expansion",
|
||||
"reference": "https://github.com/meap158/ComfyUI-Prompt-Expansion",
|
||||
"files": [
|
||||
"https://github.com/meap158/ComfyUI-Prompt-Expansion"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Dynamic prompt expansion, powered by GPT-2 locally on your device."
|
||||
},
|
||||
{
|
||||
"author": "BiffMunky",
|
||||
"title": "Endless \ufe0f\ud83c\udf0a\ud83c\udf20 Node \ud83c\udf0c",
|
||||
"reference": "https://github.com/tusharbhutt/Endless-Nodes",
|
||||
"files": [
|
||||
"https://github.com/tusharbhutt/Endless-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A small set of nodes I created for various numerical and text inputs. Features switches for text and numbers, parameter collection nodes, and two aesthetic scoring models."
|
||||
},
|
||||
{
|
||||
"author": "chrisgoringe",
|
||||
"title": "Variation seeds",
|
||||
"reference": "https://github.com/chrisgoringe/cg-noise",
|
||||
"files": [
|
||||
"https://github.com/chrisgoringe/cg-noise"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Adds KSampler custom nodes with variation seed and variation strength."
|
||||
},
|
||||
{
|
||||
"author": "spinagon",
|
||||
"title": "Seamless tiling Node for ComfyUI",
|
||||
"reference": "https://github.com/spinagon/ComfyUI-seamless-tiling",
|
||||
"files": [
|
||||
"https://github.com/spinagon/ComfyUI-seamless-tiling"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Node for generating almost seamless textures, based on similar setting from A1111."
|
||||
},
|
||||
{
|
||||
"author": "ramyma",
|
||||
"title": "A8R8 ComfyUI Nodes",
|
||||
"reference": "https://github.com/ramyma/A8R8_ComfyUI_nodes",
|
||||
"files": [
|
||||
"https://github.com/ramyma/A8R8_ComfyUI_nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Base64Image Input Node, Base64Image Output Node. <a href='https://github.com/ramyma/a8r8' target='blank'>A8R8</a> supporting nodes to integrate with ComfyUI"
|
||||
},
|
||||
{
|
||||
"author": "Zuellni",
|
||||
"title": "ComfyUI-ExLlama",
|
||||
"reference": "https://github.com/Zuellni/ComfyUI-ExLlama",
|
||||
"files": [
|
||||
"https://github.com/Zuellni/ComfyUI-ExLlama"
|
||||
],
|
||||
"pip": ["sentencepiece", "https://github.com/jllllll/exllama/releases/download/0.0.17/exllama-0.0.17+cu118-cp310-cp310-win_amd64.whl"],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: ExLlama Loader, ExLlama Generator. <BR>Used to load 4-bit GPTQ Llama/2 models. You can find a lot of them over at <a href='https://huggingface.co/TheBloke'>https://huggingface.co/TheBloke</a><p style='background-color: black; color: red;'>NOTE: You need to manually install a pip package that suits your system. For example. If your system is 'Python3.10 + Windows + CUDA 11.8' then you need to install 'exllama-0.0.17+cu118-cp310-cp310-win_amd64.whl'. Available package files are <a href='https://github.com/jllllll/exllama/releases'>here</a>."
|
||||
},
|
||||
{
|
||||
"author": "budihartono",
|
||||
"title": "Otonx's Custom Nodes",
|
||||
"reference": "https://github.com/budihartono/comfyui_otonx_nodes",
|
||||
"files": [
|
||||
"https://github.com/budihartono/comfyui_otonx_nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: OTX Multiple Values, OTX KSampler Feeder. This extension provides custom nodes for ComfyUI created for personal projects. Made available for reference. Nodes may be updated or changed intermittently or not at all. Review & test before use."
|
||||
},
|
||||
{
|
||||
"author": "bvhari",
|
||||
"title": "ComfyUI_PerpWeight",
|
||||
"reference": "https://github.com/bvhari/ComfyUI_PerpWeight",
|
||||
"files": [
|
||||
"https://github.com/bvhari/ComfyUI_PerpWeight"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A novel weighting scheme for token vectors from CLIP. Allows a wider range of values for the weight. Inspired by Perp-Neg."
|
||||
},
|
||||
{
|
||||
"author": "WASasquatch",
|
||||
"title": "Power Noise Suite for ComfyUI",
|
||||
"reference": "https://github.com/WASasquatch/PowerNoiseSuite",
|
||||
"files": [
|
||||
"https://github.com/WASasquatch/PowerNoiseSuite"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Power Noise Suite contains nodes centered around latent noise input, and diffusion, as well as latent adjustments."
|
||||
},
|
||||
{
|
||||
"author": "aimingfail",
|
||||
"title": "Image2Halftone Node for ComfyUI",
|
||||
"reference": "https://civitai.com/models/143293/image2halftone-node-for-comfyui",
|
||||
"files": [
|
||||
"https://civitai.com/api/download/models/158997"
|
||||
],
|
||||
"install_type": "unzip",
|
||||
"description": "This is a node to convert an image into a CMYK Halftone dot image."
|
||||
},
|
||||
{
|
||||
"author": "M1kep",
|
||||
"title": "KepPromptLang",
|
||||
"reference": "https://github.com/M1kep/KepPromptLang",
|
||||
"files": [
|
||||
"https://github.com/M1kep/KepPromptLang"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Build Gif, Special CLIP Loader. It offers various manipulation capabilities for the internal operations of the prompt."
|
||||
},
|
||||
{
|
||||
"author": "jmkl",
|
||||
"title": "ComfyUI Ricing",
|
||||
"reference": "https://github.com/jmkl/ComfyUI-ricing",
|
||||
"files": [
|
||||
"https://github.com/jmkl/ComfyUI-ricing"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom user.css and some script stuff. mainly for web interface."
|
||||
},
|
||||
{
|
||||
"author": "M1kep",
|
||||
"title": "Comfy_KepListStuff",
|
||||
"reference": "https://github.com/M1kep/Comfy_KepListStuff",
|
||||
"files": [
|
||||
"https://github.com/M1kep/Comfy_KepListStuff"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Range(Step), Range(Num Steps), List Length, Image Overlay, Stack Images, Empty Images, Join Image Lists, Join Float Lists. This extension provides various list manipulation nodes"
|
||||
},
|
||||
{
|
||||
"author": "Clybius",
|
||||
"title": "ComfyUI-Latent-Modifiers",
|
||||
"reference": "https://github.com/Clybius/ComfyUI-Latent-Modifiers",
|
||||
"files": [
|
||||
"https://github.com/Clybius/ComfyUI-Latent-Modifiers"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Latent Diffusion Mega Modifier. ComfyUI nodes which modify the latent during the diffusion process. (Sharpness, Tonemap, Rescale, Extra Noise)"
|
||||
},
|
||||
{
|
||||
"author": "ali1234",
|
||||
"title": "comfyui-job-iterator",
|
||||
"reference": "https://github.com/ali1234/comfyui-job-iterator",
|
||||
"files": [
|
||||
"https://github.com/ali1234/comfyui-job-iterator"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Implements iteration over sequences within a single workflow run. <p style='background-color: black; color: red;'>NOTE: This node replaces the execution of ComfyUI for iterative processing functionality.</p>"
|
||||
},
|
||||
{
|
||||
"author": "Tropfchen",
|
||||
"title": "Embedding Picker",
|
||||
"reference": "https://github.com/Tropfchen/ComfyUI-Embedding_Picker",
|
||||
"files": [
|
||||
"https://github.com/Tropfchen/ComfyUI-Embedding_Picker"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Tired of forgetting and misspelling often weird names of embeddings you use? Or perhaps you use only one, cause you forgot you have tens of them installed?"
|
||||
},
|
||||
{
|
||||
"author": "Kosinkadink",
|
||||
"title": "AnimateDiff Evolved",
|
||||
"reference": "https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved",
|
||||
"files": [
|
||||
"https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A forked repository that actively maintains <a href='https://github.com/ArtVentureX/comfyui-animatediff' target='blank'>AnimateDiff</a>, created by ArtVentureX.<BR><BR>Improved AnimateDiff integration for ComfyUI, adapts from sd-webui-animatediff.<br><p style='background-color: black; color: red;'>Download one or more motion models from <a href='https://huggingface.co/guoyww/animatediff/tree/main' target='blank'>Original Models</a> | <a href='https://huggingface.co/manshoety/AD_Stabilized_Motion/tree/main' target='blank'>Finetuned Models</a>. See README for additional model links and usage. Put the model weights under <font color='white'>ComfyUI/custom_nodes/ComfyUI-AnimateDiff-Evolved/models</font>. You are free to rename the models, but keeping original names will ease use when sharing your workflow.</p>"
|
||||
},
|
||||
{
|
||||
"author": "bvhari",
|
||||
"title": "ComfyUI_PerpNeg [WIP]",
|
||||
"reference": "https://github.com/bvhari/ComfyUI_PerpNeg",
|
||||
"files": [
|
||||
"https://github.com/bvhari/ComfyUI_PerpNeg"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: KSampler (Advanced + Perp-Neg). Implementation of <a href='https://perp-neg.github.io/' target='blank'>Perp-Neg</a><br>Includes Tonemap and CFG Rescale optionsComfyUI custom node to convert latent to RGB.<p style='background-color: black; color: red;'>WARNING: Experimental code, might have incompatibilities and edge cases.</>"
|
||||
},
|
||||
{
|
||||
"author": "hayden-fr",
|
||||
"title": "ComfyUI-Model-Manager",
|
||||
"reference": "https://github.com/hayden-fr/ComfyUI-Model-Manager",
|
||||
"files": [
|
||||
"https://github.com/hayden-fr/ComfyUI-Model-Manager"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Manage models: browsing, donwload and delete."
|
||||
},
|
||||
{
|
||||
"author": "wolfden",
|
||||
"title": "ComfyUi_String_Function_Tree",
|
||||
"reference": "https://github.com/wolfden/ComfyUi_String_Function_Tree",
|
||||
"files": [
|
||||
"https://github.com/wolfden/ComfyUi_String_Function_Tree"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This custom node provides the capability to manipulate multiple string inputs."
|
||||
},
|
||||
{
|
||||
"author": "city96",
|
||||
"title": "ComfyUI_DiT [WIP]",
|
||||
"reference": "https://github.com/city96/ComfyUI_DiT",
|
||||
"files": [
|
||||
"https://github.com/city96/ComfyUI_DiT"
|
||||
],
|
||||
"pip": ["huggingface-hub"],
|
||||
"install_type": "git-clone",
|
||||
"description": "Testbed for <a href='https://github.com/facebookresearch/DiT' target='blank'>DiT(Scalable Diffusion Models with Transformers)</a>. <p style='background-color: black; color: red;'>None of this code is stable, expect breaking changes if for some reason you want to use this.</p>"
|
||||
},
|
||||
{
|
||||
"author": "braintacles",
|
||||
"title": "braintacles-nodes",
|
||||
"reference": "https://github.com/braintacles/braintacles-comfyui-nodes",
|
||||
"files": [
|
||||
"https://github.com/braintacles/braintacles-comfyui-nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: CLIPTextEncodeSDXL-Multi-IO, CLIPTextEncodeSDXL-Pipe, Empty Latent Image from Aspect-Ratio, Random Find and Replace."
|
||||
},
|
||||
{
|
||||
"author": "mav-rik",
|
||||
"title": "Facerestore CF (Code Former)",
|
||||
"reference": "https://github.com/mav-rik/facerestore_cf",
|
||||
"files": [
|
||||
"https://github.com/mav-rik/facerestore_cf"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This is a copy of <a href='https://civitai.com/models/24690/comfyui-facerestore-node' target='blank'>facerestore custom node</a> with a bit of a change to support CodeFormer Fidelity parameter. These ComfyUI nodes can be used to restore faces in images similar to the face restore option in AUTOMATIC1111 webui.<BR>NOTE: To use this node, you need to download the face restoration model and face detection model from the 'Install models' menu."
|
||||
},
|
||||
{
|
||||
"author": "ArtBot2023",
|
||||
"title": "Character Face Swap",
|
||||
"reference": "https://github.com/ArtBot2023/CharacterFaceSwap",
|
||||
"files": [
|
||||
"https://github.com/ArtBot2023/CharacterFaceSwap"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Character face swap with LoRA and embeddings."
|
||||
},
|
||||
{
|
||||
"author": "cubiq",
|
||||
"title": "ComfyUI_IPAdapter_plus",
|
||||
"reference": "https://github.com/cubiq/ComfyUI_IPAdapter_plus",
|
||||
"files": [
|
||||
"https://github.com/cubiq/ComfyUI_IPAdapter_plus"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI reference implementation for IPAdapter models. The code is mostly taken from the original IPAdapter repository and laksjdjf's implementation, all credit goes to them. I just made the extension closer to ComfyUI philosophy."
|
||||
},
|
||||
{
|
||||
"author": "ealkanat",
|
||||
"title": "ComfyUI Easy Padding",
|
||||
"reference": "https://github.com/ealkanat/comfyui_easy_padding",
|
||||
"files": [
|
||||
"https://github.com/ealkanat/comfyui_easy_padding"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI Easy Padding is a simple custom ComfyUI node that helps you to add padding to images on ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "seanlynch",
|
||||
"title": "ComfyUI Optical Flow",
|
||||
"reference": "https://github.com/seanlynch/comfyui-optical-flow",
|
||||
"files": [
|
||||
"https://github.com/seanlynch/comfyui-optical-flow"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This package contains three nodes to help you compute optical flow between pairs of images, usually adjacent frames in a video, visualize the flow, and apply the flow to another image of the same dimensions. Most of the code is from Deforum, so this is released under the same license (MIT)."
|
||||
},
|
||||
{
|
||||
"author": "bmad4ever",
|
||||
"title": "ComfyUI-Bmad-Custom-Nodes",
|
||||
"reference": "https://github.com/bmad4ever/ComfyUI-Bmad-Custom-Nodes",
|
||||
"files": [
|
||||
"https://github.com/bmad4ever/ComfyUI-Bmad-Custom-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This custom node offers the following functionalities: API support for setting up API requests, computer vision primarily for masking purposes using GrabCut or contours, and general utility to streamline workflow setup or implement essential missing features."
|
||||
},
|
||||
{
|
||||
"author": "RockOfFire",
|
||||
"title": "CR Animation Nodes",
|
||||
"reference": "https://github.com/RockOfFire/CR_Animation_Nodes",
|
||||
"files": [
|
||||
"https://github.com/RockOfFire/CR_Animation_Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A comprehensive suite of nodes to enhance your animations. These nodes include some features similar to Deforum, and also some new ideas."
|
||||
},
|
||||
{
|
||||
"author": "Dr.Lt.Data",
|
||||
"title": "ComfyUI Inspire Pack",
|
||||
"reference": "https://github.com/ltdrdata/ComfyUI-Inspire-Pack",
|
||||
"files": [
|
||||
"https://github.com/ltdrdata/ComfyUI-Inspire-Pack"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension provides various nodes to support Lora Block Weight and the Impact Pack."
|
||||
},
|
||||
{
|
||||
"author": "wolfden",
|
||||
"title": "SDXL Prompt Styler (customized version by wolfden)",
|
||||
"reference": "https://github.com/wolfden/ComfyUi_PromptStylers",
|
||||
"files": [
|
||||
"https://github.com/wolfden/ComfyUi_PromptStylers"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "These custom nodes provide a variety of customized prompt stylers based on <a href='https://github.com/twri/sdxl_prompt_styler'>twri/SDXL Prompt Styler</a>."
|
||||
},
|
||||
{
|
||||
"author": "Dream Project",
|
||||
"title": "Dream Project Animation Nodes",
|
||||
"reference": "https://github.com/alt-key-project/comfyui-dream-project",
|
||||
"files": [
|
||||
"https://github.com/alt-key-project/comfyui-dream-project"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension offers various nodes that are useful for Deforum-like animations in ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "comfyanonymous",
|
||||
"title": "ComfyUI_experiments",
|
||||
"reference": "https://github.com/comfyanonymous/ComfyUI_experiments",
|
||||
"files": [
|
||||
"https://github.com/comfyanonymous/ComfyUI_experiments"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: ModelSamplerTonemapNoiseTest, ReferenceOnlySimple, RescaleClassifierFreeGuidanceTest, ModelMergeBlockNumber, ModelMergeSDXL, ModelMergeSDXLTransformers, ModelMergeSDXLDetailedTransformers.<p style='background-color: black; color: red;'>This is a consolidation of the previously separate custom nodes. Please delete the sampler_tonemap.py, sampler_rescalecfg.py, advanced_model_merging.py, sdxl_model_merging.py, and reference_only.py files installed in custom_nodes before.</p>"
|
||||
},
|
||||
{
|
||||
"author": "ssitu",
|
||||
"title": "ComfyUI fabric",
|
||||
"reference": "https://github.com/ssitu/ComfyUI_fabric",
|
||||
"files": [
|
||||
"https://github.com/ssitu/ComfyUI_fabric"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI nodes based on the paper '<a href='https://arxiv.org/abs/2307.10159'/>FABRIC: Personalizing Diffusion Models with Iterative Feedback</a>' (Feedback via Attention-Based Reference Image Conditioning)"
|
||||
},
|
||||
{
|
||||
"author": "picturesonpictures",
|
||||
"title": "comfy_PoP",
|
||||
"reference": "https://github.com/picturesonpictures/comfy_PoP",
|
||||
"files": ["https://github.com/picturesonpictures/comfy_PoP"],
|
||||
"install_type": "git-clone",
|
||||
"description": "A collection of custom nodes for ComfyUI. Includes a quick canny edge detection node with unconventional settings, simple LoRA stack nodes for workflow efficiency, and a customizable aspect ratio node."
|
||||
},
|
||||
{
|
||||
"author": "Acly",
|
||||
"title": "ComfyUI Nodes for External Tooling",
|
||||
"reference": "https://github.com/Acly/comfyui-tooling-nodes",
|
||||
"files": [
|
||||
"https://github.com/Acly/comfyui-tooling-nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Load Image (Base64), Load Mask (Base64), Send Image (WebSocket), Crop Image, Apply Mask to Image. Provides nodes geared towards using ComfyUI as a backend for external tools."
|
||||
},
|
||||
{
|
||||
"author": "ntdviet",
|
||||
"title": "ntdviet/comfyui-ext",
|
||||
"reference": "https://github.com/ntdviet/comfyui-ext",
|
||||
"files": [
|
||||
"https://github.com/ntdviet/comfyui-ext/raw/main/custom_nodes/gcLatentTunnel/gcLatentTunnel.py"
|
||||
],
|
||||
"install_type": "copy",
|
||||
"description": "Nodes:LatentGarbageCollector. This ComfyUI custom node flushes the GPU cache and empty cuda interprocess memory. It's helpfull for low memory environment such as the free Google Colab, especially when the workflow VAE decode latents of the size above 1500x1500."
|
||||
},
|
||||
{
|
||||
"author": "laksjdjf",
|
||||
"title": "attention-couple-ComfyUI",
|
||||
"reference": "https://github.com/laksjdjf/attention-couple-ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/laksjdjf/attention-couple-ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Attention couple. This is a custom node that manipulates region-specific prompts. While vanilla ComfyUI employs an area specification method based on latent couples, this node divides regions using attention layers within UNet."
|
||||
},
|
||||
{
|
||||
"author": "spro",
|
||||
"title": "Latent Mirror node for ComfyUI",
|
||||
"reference": "https://github.com/spro/comfyui-mirror",
|
||||
"files": [
|
||||
"https://github.com/spro/comfyui-mirror"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Latent Mirror. Node to mirror a latent along the Y (vertical / left to right) or X (horizontal / top to bottom) axis."
|
||||
},
|
||||
{
|
||||
"author": "WASasquatch",
|
||||
"title": "PPF_Noise_ComfyUI",
|
||||
"reference": "https://github.com/WASasquatch/PPF_Noise_ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/WASasquatch/PPF_Noise_ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: WAS_PFN_Latent. Perlin Power Fractal Noisey Latents"
|
||||
},
|
||||
{
|
||||
"author": "Ttl",
|
||||
"title": "ComfyUI Neural network latent upscale custom node",
|
||||
"reference": "https://github.com/Ttl/ComfyUi_NNLatentUpscale",
|
||||
"files": [
|
||||
"https://github.com/Ttl/ComfyUi_NNLatentUpscale"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A custom ComfyUI node designed for rapid latent upscaling using a compact neural network, eliminating the need for VAE-based decoding and encoding."
|
||||
},
|
||||
{
|
||||
"author": "GeLi1989",
|
||||
"title": "roop nodes for ComfyUI",
|
||||
"reference": "https://github.com/GeLi1989/GK-beifen-ComfyUI_roop",
|
||||
"files": [
|
||||
"https://github.com/GeLi1989/GK-beifen-ComfyUI_roop"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI nodes for the roop A1111 webui script."
|
||||
},
|
||||
{
|
||||
"author": "Onierous",
|
||||
"title": "QRNG_Node_ComfyUI",
|
||||
"reference": "https://github.com/Onierous/QRNG_Node_ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/Onierous/QRNG_Node_ComfyUI/raw/main/qrng_node.py"
|
||||
],
|
||||
"install_type": "copy",
|
||||
"description": "Nodes: QRNG Node CSV. A node that takes in an array of random numbers from the ANU QRNG API and stores them locally for generating quantum random number noise_seeds in ComfyUI"
|
||||
},
|
||||
{
|
||||
"author": "Lerc",
|
||||
"title": "Canvas Tab",
|
||||
"reference": "https://github.com/Lerc/canvas_tab",
|
||||
"files": [
|
||||
"https://github.com/Lerc/canvas_tab"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension provides a full page image editor with mask support. There are two nodes, one to receive images from the editor and one to send images to the editor."
|
||||
},
|
||||
{
|
||||
"author": "YOUR-WORST-TACO",
|
||||
"title": "ComfyUI-TacoNodes",
|
||||
"reference": "https://github.com/YOUR-WORST-TACO/ComfyUI-TacoNodes",
|
||||
"files": [
|
||||
"https://github.com/YOUR-WORST-TACO/ComfyUI-TacoNodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:TacoLatent, TacoAnimatedLoader, TacoImg2ImgAnimatedLoader, TacoGifMaker."
|
||||
},
|
||||
{
|
||||
"author": "skfoo",
|
||||
"title": "ComfyUI-Coziness",
|
||||
"reference": "https://github.com/skfoo/ComfyUI-Coziness",
|
||||
"files": [
|
||||
"https://github.com/skfoo/ComfyUI-Coziness"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:MultiLora Loader, Lora Text Extractor. Provides a node for assisting in loading loras through text."
|
||||
},
|
||||
{
|
||||
"author": "Fannovel16",
|
||||
"title": "ComfyUI Loopchain",
|
||||
"reference": "https://github.com/Fannovel16/ComfyUI-Loopchain",
|
||||
"files": [
|
||||
"https://github.com/Fannovel16/ComfyUI-Loopchain"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A collection of nodes which can be useful for animation in ComfyUI. The main focus of this extension is implementing a mechanism called loopchain. A loopchain in this case is the chain of nodes only executed repeatly in the workflow. If a node chain contains a loop node from this extension, it will become a loop chain."
|
||||
},
|
||||
{
|
||||
"author": "Sxela",
|
||||
"title": "ComfyWarp",
|
||||
"reference": "https://github.com/Sxela/ComfyWarp",
|
||||
"files": [
|
||||
"https://github.com/Sxela/ComfyWarp"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:LoadFrameSequence, LoadFrame"
|
||||
},
|
||||
{
|
||||
"author": "Beinsezii",
|
||||
"title": "bsz-cui-extras",
|
||||
"reference": "https://github.com/Beinsezii/bsz-cui-extras",
|
||||
"files": [
|
||||
"https://github.com/Beinsezii/bsz-cui-extras"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension consists of auxiliary nodes for automatic calculation of latent sizes, an all-in-one node for SDXL's t2i, i2i, and scaling/hiresfix, and nodes capable of loading the Pixelbuster library.<p style='background-color: black; color: red;'>NOTE:Currently, the Pixelbuster library includes libraries for Windows and Linux environments by default. For other environments such as OSX, you will need to build and use Pixelbuster directly from <a href='https://github.com/Beinsezii/pixelbuster' target='blank'>here</a>.</P>"
|
||||
},
|
||||
{
|
||||
"author": "richinsley",
|
||||
"title": "Comfy-LFO",
|
||||
"reference": "https://github.com/richinsley/Comfy-LFO",
|
||||
"files": [
|
||||
"https://github.com/richinsley/Comfy-LFO"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:LFO_Triangle, LFO_Sine, SawtoothNode, SquareNode, PulseNode. ComfyUI custom nodes to create Low Frequency Oscillators."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -53,8 +53,7 @@
|
||||
" !git pull\n",
|
||||
"\n",
|
||||
"!echo -= Install dependencies =-\n",
|
||||
"#!pip install xformers!=0.0.18 -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu118 --extra-index-url https://download.pytorch.org/whl/cu117\n",
|
||||
"!pip install xformers!=0.0.18 torch==2.0.1 torchsde einops transformers>=4.25.1 safetensors>=0.3.0 aiohttp accelerate pyyaml Pillow scipy tqdm psutil --extra-index-url https://download.pytorch.org/whl/cu118 --extra-index-url https://download.pytorch.org/whl/cu117\n",
|
||||
"!pip install xformers!=0.0.18 -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu121 --extra-index-url https://download.pytorch.org/whl/cu118 --extra-index-url https://download.pytorch.org/whl/cu117\n",
|
||||
"\n",
|
||||
"if OPTIONS['USE_COMFYUI_MANAGER']:\n",
|
||||
" %cd custom_nodes\n",
|
||||
|
||||
@ -5,6 +5,7 @@ import sys
|
||||
import atexit
|
||||
import threading
|
||||
import re
|
||||
import locale
|
||||
|
||||
|
||||
message_collapses = []
|
||||
@ -17,6 +18,45 @@ def register_message_collapse(f):
|
||||
|
||||
sys.__comfyui_manager_register_message_collapse = register_message_collapse
|
||||
|
||||
comfyui_manager_path = os.path.dirname(__file__)
|
||||
custom_nodes_path = os.path.join(comfyui_manager_path, "..")
|
||||
startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts")
|
||||
restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json")
|
||||
git_script_path = os.path.join(comfyui_manager_path, "git_helper.py")
|
||||
|
||||
|
||||
def handle_stream(stream, prefix):
|
||||
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
|
||||
for msg in stream:
|
||||
if prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg):
|
||||
if msg.startswith('100%'):
|
||||
print('\r' + msg, end="", file=sys.stderr),
|
||||
else:
|
||||
print('\r' + msg[:-1], end="", file=sys.stderr),
|
||||
else:
|
||||
if prefix == '[!]':
|
||||
print(prefix, msg, end="", file=sys.stderr)
|
||||
else:
|
||||
print(prefix, msg, end="")
|
||||
|
||||
|
||||
def process_wrap(cmd_str, cwd_path, handler=None):
|
||||
process = subprocess.Popen(cmd_str, cwd=cwd_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
|
||||
|
||||
if handler is None:
|
||||
handler = handle_stream
|
||||
|
||||
stdout_thread = threading.Thread(target=handler, args=(process.stdout, ""))
|
||||
stderr_thread = threading.Thread(target=handler, args=(process.stderr, "[!]"))
|
||||
|
||||
stdout_thread.start()
|
||||
stderr_thread.start()
|
||||
|
||||
stdout_thread.join()
|
||||
stderr_thread.join()
|
||||
|
||||
return process.wait()
|
||||
|
||||
|
||||
try:
|
||||
if '--port' in sys.argv:
|
||||
@ -96,11 +136,6 @@ try:
|
||||
original_stderr.flush()
|
||||
|
||||
|
||||
def handle_stream(stream, prefix):
|
||||
for line in stream:
|
||||
print(prefix, line, end="")
|
||||
|
||||
|
||||
def close_log():
|
||||
log_file.close()
|
||||
|
||||
@ -116,6 +151,82 @@ except Exception as e:
|
||||
print("** ComfyUI start up time:", datetime.datetime.now())
|
||||
|
||||
|
||||
if os.path.exists(restore_snapshot_path):
|
||||
try:
|
||||
import json
|
||||
|
||||
cloned_repos = []
|
||||
|
||||
def msg_capture(stream, prefix):
|
||||
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
|
||||
for msg in stream:
|
||||
if msg.startswith("CLONE: "):
|
||||
cloned_repos.append(msg[7:])
|
||||
if prefix == '[!]':
|
||||
print(prefix, msg, end="", file=sys.stderr)
|
||||
else:
|
||||
print(prefix, msg, end="")
|
||||
|
||||
elif prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg):
|
||||
if msg.startswith('100%'):
|
||||
print('\r' + msg, end="", file=sys.stderr),
|
||||
else:
|
||||
print('\r'+msg[:-1], end="", file=sys.stderr),
|
||||
else:
|
||||
if prefix == '[!]':
|
||||
print(prefix, msg, end="", file=sys.stderr)
|
||||
else:
|
||||
print(prefix, msg, end="")
|
||||
|
||||
print(f"[ComfyUI-Manager] Restore snapshot.")
|
||||
cmd_str = [sys.executable, git_script_path, '--apply-snapshot', restore_snapshot_path]
|
||||
exit_code = process_wrap(cmd_str, custom_nodes_path, handler=msg_capture)
|
||||
|
||||
with open(restore_snapshot_path, 'r', encoding="UTF-8") as json_file:
|
||||
info = json.load(json_file)
|
||||
for url in cloned_repos:
|
||||
try:
|
||||
repository_name = url.split("/")[-1].strip()
|
||||
repo_path = os.path.join(custom_nodes_path, repository_name)
|
||||
repo_path = os.path.abspath(repo_path)
|
||||
|
||||
requirements_path = os.path.join(repo_path, 'requirements.txt')
|
||||
install_script_path = os.path.join(repo_path, 'install.py')
|
||||
|
||||
this_exit_code = 0
|
||||
|
||||
if os.path.exists(requirements_path):
|
||||
with open(requirements_path, 'r', encoding="UTF-8") as file:
|
||||
for line in file:
|
||||
package_name = line.strip()
|
||||
if package_name:
|
||||
install_cmd = [sys.executable, "-m", "pip", "install", package_name]
|
||||
this_exit_code += process_wrap(install_cmd, repo_path)
|
||||
|
||||
if os.path.exists(install_script_path):
|
||||
install_cmd = [sys.executable, install_script_path]
|
||||
print(f">>> {install_cmd} / {repo_path}")
|
||||
this_exit_code += process_wrap(install_cmd, repo_path)
|
||||
|
||||
if this_exit_code != 0:
|
||||
print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
|
||||
|
||||
if exit_code != 0:
|
||||
print(f"[ComfyUI-Manager] Restore snapshot failed.")
|
||||
else:
|
||||
print(f"[ComfyUI-Manager] Restore snapshot done.")
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(f"[ComfyUI-Manager] Restore snapshot failed.")
|
||||
|
||||
os.remove(restore_snapshot_path)
|
||||
|
||||
|
||||
# Perform install
|
||||
script_list_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "startup-scripts", "install-scripts.txt")
|
||||
|
||||
@ -126,7 +237,7 @@ if os.path.exists(script_list_path):
|
||||
|
||||
executed = set()
|
||||
# Read each line from the file and convert it to a list using eval
|
||||
with open(script_list_path, 'r') as file:
|
||||
with open(script_list_path, 'r', encoding="UTF-8") as file:
|
||||
for line in file:
|
||||
if line in executed:
|
||||
continue
|
||||
@ -135,24 +246,17 @@ if os.path.exists(script_list_path):
|
||||
|
||||
try:
|
||||
script = eval(line)
|
||||
print(f"\n## ComfyUI-Manager: EXECUTE => {script[1:]}")
|
||||
if os.path.exists(script[0]):
|
||||
print(f"\n## ComfyUI-Manager: EXECUTE => {script[1:]}")
|
||||
|
||||
print(f"\n## Execute install/(de)activation script for '{script[0]}'")
|
||||
process = subprocess.Popen(script[1:], cwd=script[0], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
|
||||
print(f"\n## Execute install/(de)activation script for '{script[0]}'")
|
||||
exit_code = process_wrap(script[1:], script[0])
|
||||
|
||||
stdout_thread = threading.Thread(target=handle_stream, args=(process.stdout, ""))
|
||||
stderr_thread = threading.Thread(target=handle_stream, args=(process.stderr, "[!]"))
|
||||
if exit_code != 0:
|
||||
print(f"install/(de)activation script failed: {script[0]}")
|
||||
else:
|
||||
print(f"\n## ComfyUI-Manager: CANCELED => {script[1:]}")
|
||||
|
||||
stdout_thread.start()
|
||||
stderr_thread.start()
|
||||
|
||||
stdout_thread.join()
|
||||
stderr_thread.join()
|
||||
|
||||
exit_code = process.wait()
|
||||
|
||||
if exit_code != 0:
|
||||
print(f"install/(de)activation script failed: {script[0]}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to execute install/(de)activation script: {line} / {e}")
|
||||
|
||||
|
||||
13
scanner.py
13
scanner.py
@ -3,6 +3,7 @@ import os
|
||||
import json
|
||||
from git import Repo
|
||||
from torchvision.datasets.utils import download_url
|
||||
import concurrent
|
||||
|
||||
builtin_nodes = ["KSampler", "CheckpointSave"]
|
||||
|
||||
@ -162,7 +163,7 @@ def update_custom_nodes():
|
||||
|
||||
git_url_titles = get_git_urls_from_json('custom-node-list.json')
|
||||
|
||||
for url, title in git_url_titles:
|
||||
def process_git_url_title(url, title):
|
||||
name = os.path.basename(url)
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
@ -170,9 +171,14 @@ def update_custom_nodes():
|
||||
node_info[name] = (url, title)
|
||||
clone_or_pull_git_repository(url)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(10) as executor:
|
||||
for url, title in git_url_titles:
|
||||
executor.submit(process_git_url_title, url, title)
|
||||
|
||||
py_url_titles = get_py_urls_from_json('custom-node-list.json')
|
||||
|
||||
for url, title in py_url_titles:
|
||||
def download_and_store_info(url_title):
|
||||
url, title = url_title
|
||||
name = os.path.basename(url)
|
||||
if name.endswith(".py"):
|
||||
node_info[name] = (url, title)
|
||||
@ -182,6 +188,9 @@ def update_custom_nodes():
|
||||
except:
|
||||
print(f"[ERROR] Cannot download '{url}'")
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(10) as executor:
|
||||
executor.map(download_and_store_info, py_url_titles)
|
||||
|
||||
return node_info
|
||||
|
||||
|
||||
|
||||
0
snapshots/the_snapshot_files_are_located_here
Normal file
0
snapshots/the_snapshot_files_are_located_here
Normal file
Loading…
Reference in New Issue
Block a user