diff --git a/README.md b/README.md index 9df5886a..229ff078 100644 --- a/README.md +++ b/README.md @@ -192,14 +192,14 @@ NODE_CLASS_MAPPINGS.update({ ## Roadmap -- [ ] System displaying information about failed custom nodes import. -- [ ] Guide for missing nodes in ComfyUI vanilla nodes. +- [x] System displaying information about failed custom nodes import. +- [x] Guide for missing nodes in ComfyUI vanilla nodes. +- [x] Collision checking system for nodes with the same ID across extensions. - [ ] Auto migration for custom nodes with changed structures. - [ ] Version control feature for nodes. - [ ] List of currently used custom nodes. - [ ] Template sharing system. - [ ] 3rd party API system. -- [ ] Collision checking system for nodes with the same ID across extensions. # Disclaimer diff --git a/__init__.py b/__init__.py index 8d910f08..5bcdc1c0 100644 --- a/__init__.py +++ b/__init__.py @@ -13,8 +13,10 @@ from tqdm.auto import tqdm import concurrent import ssl from urllib.parse import urlparse +import http.client +import re -version = "V1.3" +version = "V1.4" print(f"### Loading: ComfyUI-Manager ({version})") @@ -84,6 +86,7 @@ from torchvision.datasets.utils import download_url comfy_ui_required_revision = 1240 comfy_ui_revision = "Unknown" +comfy_ui_commit_date = "" comfy_path = os.path.dirname(folder_paths.__file__) custom_nodes_path = os.path.join(comfy_path, 'custom_nodes') @@ -244,6 +247,8 @@ def try_install_script(url, repo_path, install_cmd): def print_comfyui_version(): global comfy_ui_revision + global comfy_ui_commit_date + try: repo = git.Repo(os.path.dirname(folder_paths.__file__)) @@ -257,10 +262,11 @@ def print_comfyui_version(): except: pass + comfy_ui_commit_date = repo.head.commit.committed_datetime.date() if current_branch == "master": - print(f"### ComfyUI Revision: {comfy_ui_revision} [{git_hash[:8]}] | Released on '{repo.head.commit.committed_datetime.date()}'") + print(f"### ComfyUI Revision: {comfy_ui_revision} [{git_hash[:8]}] | Released on '{comfy_ui_commit_date}'") else: - print(f"### ComfyUI Revision: {comfy_ui_revision} on '{current_branch}' [{git_hash[:8]}] | Released on '{repo.head.commit.committed_datetime.date()}'") + print(f"### ComfyUI Revision: {comfy_ui_revision} on '{current_branch}' [{git_hash[:8]}] | Released on '{comfy_ui_commit_date}'") except: print("### ComfyUI Revision: UNKNOWN (The currently installed ComfyUI is not a Git repository)") @@ -531,10 +537,15 @@ def check_a_custom_node_installed(item, do_fetch=False, do_update_check=True, do try: if do_update_check and git_repo_has_updates(dir_path, do_fetch, do_update): item['installed'] = 'Update' + elif sys.__comfyui_manager_is_import_failed_extension(dir_name): + item['installed'] = 'Fail' else: item['installed'] = 'True' except: - item['installed'] = 'True' + if sys.__comfyui_manager_is_import_failed_extension(dir_name): + item['installed'] = 'Fail' + else: + item['installed'] = 'True' elif os.path.exists(dir_path + ".disabled"): item['installed'] = 'Disabled' @@ -554,7 +565,10 @@ def check_a_custom_node_installed(item, do_fetch=False, do_update_check=True, do file_path = os.path.join(base_path, dir_name) if os.path.exists(file_path): - item['installed'] = 'True' + if sys.__comfyui_manager_is_import_failed_extension(dir_name): + item['installed'] = 'Fail' + else: + item['installed'] = 'True' elif os.path.exists(file_path + ".disabled"): item['installed'] = 'Disabled' else: @@ -1421,6 +1435,37 @@ async def channel_url_list(request): return web.Response(status=200) + +@server.PromptServer.instance.routes.get("/manager/notice") +async def get_notice(request): + url = "github.com" + path = "/ltdrdata/ltdrdata.github.io/wiki/News" + + conn = http.client.HTTPSConnection(url) + conn.request("GET", path) + + response = conn.getresponse() + + try: + if response.status == 200: + html_content = response.read().decode('utf-8') + + pattern = re.compile(r'
([\s\S]*?)
') + match = pattern.search(html_content) + + if match: + markdown_content = match.group(1) + markdown_content += f"
ComfyUI: {comfy_ui_revision} ({comfy_ui_commit_date})" + markdown_content += f"
Manager: {version}" + return web.Response(text=markdown_content, status=200) + else: + return web.Response(text="Unable to retrieve Notice", status=200) + else: + return web.Response(text="Unable to retrieve Notice", status=200) + finally: + conn.close() + + def get_matrix_auth(): if not os.path.exists(os.path.join(folder_paths.base_path, "matrix_auth")): return None diff --git a/extension-node-map.json b/extension-node-map.json index 1e874ed0..9ba5faa3 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -2565,6 +2565,138 @@ "title_aux": "comfy-nodes" } ], + "https://github.com/comfyanonymous/ComfyUI": [ + [ + "BasicScheduler", + "CLIPLoader", + "CLIPMergeSimple", + "CLIPSave", + "CLIPSetLastLayer", + "CLIPTextEncode", + "CLIPTextEncodeSDXL", + "CLIPTextEncodeSDXLRefiner", + "CLIPVisionEncode", + "CLIPVisionLoader", + "Canny", + "CheckpointLoader", + "CheckpointLoaderSimple", + "CheckpointSave", + "ConditioningAverage", + "ConditioningCombine", + "ConditioningConcat", + "ConditioningSetArea", + "ConditioningSetAreaPercentage", + "ConditioningSetMask", + "ConditioningSetTimestepRange", + "ConditioningZeroOut", + "ControlNetApply", + "ControlNetApplyAdvanced", + "ControlNetLoader", + "CropMask", + "DiffControlNetLoader", + "DiffusersLoader", + "DualCLIPLoader", + "EmptyImage", + "EmptyLatentImage", + "ExponentialScheduler", + "FeatherMask", + "FlipSigmas", + "FreeU", + "FreeU_V2", + "GLIGENLoader", + "GLIGENTextBoxApply", + "GrowMask", + "HyperTile", + "HypernetworkLoader", + "ImageBatch", + "ImageBlend", + "ImageBlur", + "ImageColorToMask", + "ImageCompositeMasked", + "ImageCrop", + "ImageInvert", + "ImageOnlyCheckpointLoader", + "ImagePadForOutpaint", + "ImageQuantize", + "ImageScale", + "ImageScaleBy", + "ImageScaleToTotalPixels", + "ImageSharpen", + "ImageToMask", + "ImageUpscaleWithModel", + "InvertMask", + "JoinImageWithAlpha", + "KSampler", + "KSamplerAdvanced", + "KSamplerSelect", + "KarrasScheduler", + "LatentAdd", + "LatentBlend", + "LatentComposite", + "LatentCompositeMasked", + "LatentCrop", + "LatentFlip", + "LatentFromBatch", + "LatentInterpolate", + "LatentMultiply", + "LatentRotate", + "LatentSubtract", + "LatentUpscale", + "LatentUpscaleBy", + "LoadImage", + "LoadImageMask", + "LoadLatent", + "LoraLoader", + "LoraLoaderModelOnly", + "MaskComposite", + "MaskToImage", + "ModelMergeAdd", + "ModelMergeBlocks", + "ModelMergeSimple", + "ModelMergeSubtract", + "ModelSamplingContinuousEDM", + "ModelSamplingDiscrete", + "PatchModelAddDownscale", + "PolyexponentialScheduler", + "PorterDuffImageComposite", + "PreviewImage", + "RebatchLatents", + "RepeatImageBatch", + "RepeatLatentBatch", + "RescaleCFG", + "SVD_img2vid_Conditioning", + "SamplerCustom", + "SamplerDPMPP_2M_SDE", + "SamplerDPMPP_SDE", + "SaveAnimatedPNG", + "SaveAnimatedWEBP", + "SaveImage", + "SaveLatent", + "SetLatentNoiseMask", + "SolidMask", + "SplitImageWithAlpha", + "SplitSigmas", + "StyleModelApply", + "StyleModelLoader", + "TomePatchModel", + "UNETLoader", + "UpscaleModelLoader", + "VAEDecode", + "VAEDecodeTiled", + "VAEEncode", + "VAEEncodeForInpaint", + "VAEEncodeTiled", + "VAELoader", + "VAESave", + "VPScheduler", + "VideoLinearCFGGuidance", + "unCLIPCheckpointLoader", + "unCLIPConditioning" + ], + { + "title_aux": "ComfyUI" + } + ], "https://github.com/comfyanonymous/ComfyUI_experiments": [ [ "ModelMergeBlockNumber", @@ -4274,6 +4406,7 @@ [ "Auto Merge Block Weighted", "CLIPMergeSimple", + "CheckpointSave", "ModelMergeBlocks", "ModelMergeSimple" ], diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index 914dec96..c26d16c8 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -28,6 +28,16 @@ docStyle.innerHTML = ` text-align: center; height: 45px; } + +.cm-notice-board { + width: 250px; + height: 130px; + overflow: auto; + color: var(--input-text); + border: 1px solid #ccc; + padding: 10px; + overflow-x: hidden; +}; `; document.head.appendChild(docStyle); @@ -79,6 +89,14 @@ async function init_badge_mode() { .then(data => { badge_mode = data; }) } +async function init_notice(notice) { + api.fetchApi('/manager/notice') + .then(response => response.text()) + .then(data => { + notice.innerHTML = data; + }) +} + await init_badge_mode(); @@ -436,7 +454,7 @@ class ManagerMenuDialog extends ComfyDialog { } createControlsRight() { - return [ + const elts = [ $el("button", { type: "button", textContent: "ComfyUI Community Manual", @@ -514,7 +532,16 @@ class ManagerMenuDialog extends ComfyDialog { textContent: "ComfyUI Nodes Info", onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); } }), + $el("br", {}, []), ]; + + var textarea = document.createElement("div"); + textarea.className = "cm-notice-board"; + elts.push(textarea); + + init_notice(textarea); + + return elts; } constructor() { @@ -647,6 +674,10 @@ app.registerExtension({ if (nicknames[nodeData.name.trim()]) { let nick = nicknames[nodeData.name.trim()]; + if (nick == 'ComfyUI') { + nick = "🦊" + } + if (nick.length > 25) { text += nick.substring(0, 23) + ".."; } @@ -691,6 +722,10 @@ app.registerExtension({ if (nicknames[node.type.trim()]) { let nick = nicknames[node.type.trim()]; + if (nick == 'ComfyUI') { + nick = "🦊" + } + if (nick.length > 25) { text += nick.substring(0, 23) + ".."; } diff --git a/js/custom-nodes-downloader.js b/js/custom-nodes-downloader.js index fd8553e5..a2f3f5a3 100644 --- a/js/custom-nodes-downloader.js +++ b/js/custom-nodes-downloader.js @@ -29,6 +29,53 @@ async function getCustomnodeMappings() { return data; } +async function getConflictMappings() { + 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(); + + let node_to_extensions_map = {}; + + for(let k in data) { + for(let i in data[k][0]) { + let node = data[k][0][i]; + let l = node_to_extensions_map[node]; + if(!l) { + l = []; + node_to_extensions_map[node] = l; + } + l.push(k); + } + } + + let conflict_map = {}; + for(let node in node_to_extensions_map) { + if(node_to_extensions_map[node].length > 1) { + for(let i in node_to_extensions_map[node]) { + let extension = node_to_extensions_map[node][i]; + let l = conflict_map[extension]; + + if(!l) { + l = []; + conflict_map[extension] = l; + } + + for(let j in node_to_extensions_map[node]) { + let extension2 = node_to_extensions_map[node][j]; + if(extension != extension2) + l.push([node, extension2]); + } + } + } + } + + return conflict_map; +} + async function getUnresolvedNodesInComponent() { try { var mode = "url"; @@ -180,6 +227,8 @@ export class CustomNodesInstaller extends ComfyDialog { // invalidate this.data = (await getCustomNodes()).custom_nodes; + this.conflict_mappings = await getConflictMappings(); + if(this.is_missing_node_mode) this.data = await this.filter_missing_node(this.data); @@ -369,6 +418,7 @@ export class CustomNodesInstaller extends ComfyDialog { 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" @@ -376,14 +426,43 @@ export class CustomNodesInstaller extends ComfyDialog { 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 = ` ${data.title}`; + if(data.installed == 'Fail') + data3.innerHTML = ' (IMPORT FAILED)' + data3.innerHTML; + var data4 = document.createElement('td'); data4.innerHTML = data.description; data4.className = "cm-node-desc" + + let conflicts = this.conflict_mappings[data.files[0]]; + if(conflicts) { + let buf = '

Conflicted Nodes:
'; + for(let k in conflicts) { + let node_name = conflicts[k][0]; + + let extension_name = conflicts[k][1].split('/').pop(); + if(extension_name.endsWith('/')) { + extension_name = extension_name.slice(0, -1); + } + if(node_name.endsWith('.git')) { + extension_name = extension_name.slice(0, -4); + } + + buf += `${node_name} [${extension_name}], `; + } + + if(buf.endsWith(', ')) { + buf = buf.slice(0, -2); + } + buf += "
"; + data4.innerHTML += buf; + } + var data5 = document.createElement('td'); data5.style.textAlign = "center"; @@ -424,6 +503,7 @@ export class CustomNodesInstaller extends ComfyDialog { installBtn.innerHTML = 'Uninstall'; installBtn.style.backgroundColor = 'red'; break; + case 'Fail': case 'True': installBtn3 = document.createElement('button'); installBtn3.innerHTML = 'Disable'; @@ -441,7 +521,7 @@ export class CustomNodesInstaller extends ComfyDialog { installBtn.style.color = 'white'; break; default: - installBtn.innerHTML = 'Try Install'; + installBtn.innerHTML = `Try Install${data.installed}`; installBtn.style.backgroundColor = 'Gray'; installBtn.style.color = 'white'; } @@ -479,7 +559,10 @@ export class CustomNodesInstaller extends ComfyDialog { data5.appendChild(installBtn); - dataRow.style.backgroundColor = "var(--bg-color)"; + if(data.installed == 'Fail') + dataRow.style.backgroundColor = "#880000"; + else + dataRow.style.backgroundColor = "var(--bg-color)"; dataRow.style.color = "var(--fg-color)"; dataRow.style.textAlign = "left"; @@ -548,6 +631,7 @@ export class CustomNodesInstaller extends ComfyDialog { { value:'Update', text:'Filter: update' }, { value:'True', text:'Filter: installed' }, { value:'False', text:'Filter: not-installed' }, + { value:'Fail', text:'Filter: import failed' }, ]; items.forEach(item => { diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 1e874ed0..9ba5faa3 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -2565,6 +2565,138 @@ "title_aux": "comfy-nodes" } ], + "https://github.com/comfyanonymous/ComfyUI": [ + [ + "BasicScheduler", + "CLIPLoader", + "CLIPMergeSimple", + "CLIPSave", + "CLIPSetLastLayer", + "CLIPTextEncode", + "CLIPTextEncodeSDXL", + "CLIPTextEncodeSDXLRefiner", + "CLIPVisionEncode", + "CLIPVisionLoader", + "Canny", + "CheckpointLoader", + "CheckpointLoaderSimple", + "CheckpointSave", + "ConditioningAverage", + "ConditioningCombine", + "ConditioningConcat", + "ConditioningSetArea", + "ConditioningSetAreaPercentage", + "ConditioningSetMask", + "ConditioningSetTimestepRange", + "ConditioningZeroOut", + "ControlNetApply", + "ControlNetApplyAdvanced", + "ControlNetLoader", + "CropMask", + "DiffControlNetLoader", + "DiffusersLoader", + "DualCLIPLoader", + "EmptyImage", + "EmptyLatentImage", + "ExponentialScheduler", + "FeatherMask", + "FlipSigmas", + "FreeU", + "FreeU_V2", + "GLIGENLoader", + "GLIGENTextBoxApply", + "GrowMask", + "HyperTile", + "HypernetworkLoader", + "ImageBatch", + "ImageBlend", + "ImageBlur", + "ImageColorToMask", + "ImageCompositeMasked", + "ImageCrop", + "ImageInvert", + "ImageOnlyCheckpointLoader", + "ImagePadForOutpaint", + "ImageQuantize", + "ImageScale", + "ImageScaleBy", + "ImageScaleToTotalPixels", + "ImageSharpen", + "ImageToMask", + "ImageUpscaleWithModel", + "InvertMask", + "JoinImageWithAlpha", + "KSampler", + "KSamplerAdvanced", + "KSamplerSelect", + "KarrasScheduler", + "LatentAdd", + "LatentBlend", + "LatentComposite", + "LatentCompositeMasked", + "LatentCrop", + "LatentFlip", + "LatentFromBatch", + "LatentInterpolate", + "LatentMultiply", + "LatentRotate", + "LatentSubtract", + "LatentUpscale", + "LatentUpscaleBy", + "LoadImage", + "LoadImageMask", + "LoadLatent", + "LoraLoader", + "LoraLoaderModelOnly", + "MaskComposite", + "MaskToImage", + "ModelMergeAdd", + "ModelMergeBlocks", + "ModelMergeSimple", + "ModelMergeSubtract", + "ModelSamplingContinuousEDM", + "ModelSamplingDiscrete", + "PatchModelAddDownscale", + "PolyexponentialScheduler", + "PorterDuffImageComposite", + "PreviewImage", + "RebatchLatents", + "RepeatImageBatch", + "RepeatLatentBatch", + "RescaleCFG", + "SVD_img2vid_Conditioning", + "SamplerCustom", + "SamplerDPMPP_2M_SDE", + "SamplerDPMPP_SDE", + "SaveAnimatedPNG", + "SaveAnimatedWEBP", + "SaveImage", + "SaveLatent", + "SetLatentNoiseMask", + "SolidMask", + "SplitImageWithAlpha", + "SplitSigmas", + "StyleModelApply", + "StyleModelLoader", + "TomePatchModel", + "UNETLoader", + "UpscaleModelLoader", + "VAEDecode", + "VAEDecodeTiled", + "VAEEncode", + "VAEEncodeForInpaint", + "VAEEncodeTiled", + "VAELoader", + "VAESave", + "VPScheduler", + "VideoLinearCFGGuidance", + "unCLIPCheckpointLoader", + "unCLIPConditioning" + ], + { + "title_aux": "ComfyUI" + } + ], "https://github.com/comfyanonymous/ComfyUI_experiments": [ [ "ModelMergeBlockNumber", @@ -4274,6 +4406,7 @@ [ "Auto Merge Block Weighted", "CLIPMergeSimple", + "CheckpointSave", "ModelMergeBlocks", "ModelMergeSimple" ], diff --git a/prestartup_script.py b/prestartup_script.py index 94230b3d..c86a17c3 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -9,6 +9,7 @@ import locale message_collapses = [] +import_failed_extensions = set() def register_message_collapse(f): @@ -16,10 +17,16 @@ def register_message_collapse(f): message_collapses.append(f) +def is_import_failed_extension(x): + global import_failed_extensions + return x in import_failed_extensions + + sys.__comfyui_manager_register_message_collapse = register_message_collapse +sys.__comfyui_manager_is_import_failed_extension = is_import_failed_extension comfyui_manager_path = os.path.dirname(__file__) -custom_nodes_path = os.path.join(comfyui_manager_path, "..") +custom_nodes_path = os.path.abspath(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") @@ -78,7 +85,12 @@ try: original_stdout = sys.stdout original_stderr = sys.stderr - tqdm = r'\d+%.*\[(.*?)\]' + pat_tqdm = r'\d+%.*\[(.*?)\]' + pat_import_fail = r'seconds \(IMPORT FAILED\):' + pat_custom_node = r'[/\\]custom_nodes[/\\](.*)$' + + is_start_mode = True + is_import_fail_mode = False log_file = open(f"comfyui{postfix}.log", "w", encoding="utf-8") log_lock = threading.Lock() @@ -99,11 +111,30 @@ try: raise ValueError("The object does not have a fileno method") def write(self, message): + global is_start_mode + global is_import_fail_mode + if any(f(message) for f in message_collapses): return + if is_start_mode: + if is_import_fail_mode: + match = re.search(pat_custom_node, message) + if match: + import_failed_extensions.add(match.group(1)) + is_import_fail_mode = False + else: + match = re.search(pat_import_fail, message) + if match: + is_import_fail_mode = True + else: + is_import_fail_mode = False + + if 'Starting server' in message: + is_start_mode = False + if not self.is_stdout: - match = re.search(tqdm, message) + match = re.search(pat_tqdm, message) if match: message = re.sub(r'([#|])\d', r'\1▌', message) message = re.sub('#', '█', message) diff --git a/scanner.py b/scanner.py index 37664b9a..8ac6a925 100644 --- a/scanner.py +++ b/scanner.py @@ -5,9 +5,12 @@ from git import Repo from torchvision.datasets.utils import download_url import concurrent -builtin_nodes = ["KSampler", "CheckpointSave"] +builtin_nodes = set() + + +def scan_in_file(filename, is_builtin=False): + global builtin_nodes -def scan_in_file(filename): try: with open(filename, encoding='utf-8') as file: code = file.read() @@ -63,9 +66,12 @@ def scan_in_file(filename): key, value = line[1:].strip().split(':') metadata[key.strip()] = value.strip() - for x in builtin_nodes: - if x in nodes: - nodes.remove(x) + if is_builtin: + builtin_nodes += set(nodes) + else: + for x in builtin_nodes: + if x in nodes: + nodes.remove(x) return nodes, metadata @@ -113,7 +119,9 @@ def get_git_urls_from_json(json_file): if node.get('install_type') == 'git-clone': files = node.get('files', []) if files: - git_clone_files.append((files[0],node.get('title'))) + git_clone_files.append((files[0], node.get('title'))) + + git_clone_files.append(("https://github.com/comfyanonymous/ComfyUI", "ComfyUI")) return git_clone_files @@ -190,7 +198,7 @@ def update_custom_nodes(): with concurrent.futures.ThreadPoolExecutor(10) as executor: executor.map(download_and_store_info, py_url_titles) - + return node_info @@ -198,6 +206,10 @@ def gen_json(node_info): # scan from .py file node_files, node_dirs = get_nodes(".tmp") + comfyui_path = os.path.abspath(os.path.join('.tmp', "ComfyUI")) + node_dirs.remove(comfyui_path) + node_dirs = [comfyui_path] + node_dirs + data = {} for dirname in node_dirs: py_files = get_py_file_paths(dirname) @@ -205,7 +217,7 @@ def gen_json(node_info): nodes = set() for py in py_files: - nodes_in_file, metadata_in_file = scan_in_file(py) + nodes_in_file, metadata_in_file = scan_in_file(py, dirname == "ComfyUI") nodes.update(nodes_in_file) metadata.update(metadata_in_file)