From 10ba975dc2c6a16dde70bb7b8a1de147f492d9aa Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Thu, 19 Oct 2023 22:58:19 +0900 Subject: [PATCH 01/59] update DB --- custom-node-list.json | 12 +++++++++++- extension-node-map.json | 12 ++++++++++++ node_db/new/custom-node-list.json | 12 +++++++++++- node_db/new/extension-node-map.json | 12 ++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index e31842b0..a2698edd 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2152,7 +2152,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", @@ -2244,6 +2244,16 @@ "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": "taabata", "title": "Syrian Falcon Nodes", diff --git a/extension-node-map.json b/extension-node-map.json index 0be1bfcb..0eaf2583 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -2370,6 +2370,17 @@ "title_aux": "ComfyUI-Vextra-Nodes" } ], + "https://github.com/drustan-hawk/primitive-types": [ + [ + "float", + "int", + "string", + "string_multiline" + ], + { + "title_aux": "primitive-types" + } + ], "https://github.com/ealkanat/comfyui_easy_padding": [ [ "comfyui-easy-padding" @@ -3653,6 +3664,7 @@ "ESS Float to Number", "ESS Float to String", "ESS Float to X", + "ESS Global Envoy", "ESS Image Reward", "ESS Image Reward Auto", "ESS Image Saver with JSON", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index f8f1258d..08e3c97f 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", @@ -128,7 +138,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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 0be1bfcb..0eaf2583 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -2370,6 +2370,17 @@ "title_aux": "ComfyUI-Vextra-Nodes" } ], + "https://github.com/drustan-hawk/primitive-types": [ + [ + "float", + "int", + "string", + "string_multiline" + ], + { + "title_aux": "primitive-types" + } + ], "https://github.com/ealkanat/comfyui_easy_padding": [ [ "comfyui-easy-padding" @@ -3653,6 +3664,7 @@ "ESS Float to Number", "ESS Float to String", "ESS Float to X", + "ESS Global Envoy", "ESS Image Reward", "ESS Image Reward Auto", "ESS Image Saver with JSON", From afd5c857c28a99b660008d368e9019c3eafb5aa4 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Thu, 19 Oct 2023 23:18:39 +0900 Subject: [PATCH 02/59] update DB --- custom-node-list.json | 10 ++++++++++ extension-node-map.json | 1 + node_db/new/custom-node-list.json | 10 ++++++++++ node_db/new/extension-node-map.json | 1 + 4 files changed, 22 insertions(+) diff --git a/custom-node-list.json b/custom-node-list.json index a2698edd..9291b474 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2013,6 +2013,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", diff --git a/extension-node-map.json b/extension-node-map.json index 0eaf2583..1bbe2a4a 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -2307,6 +2307,7 @@ "MaskBlur+", "MaskFlip+", "MaskPreview+", + "ModelCompile+", "SimpleMath+" ], { diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 08e3c97f..4c74bf2f 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 0eaf2583..1bbe2a4a 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -2307,6 +2307,7 @@ "MaskBlur+", "MaskFlip+", "MaskPreview+", + "ModelCompile+", "SimpleMath+" ], { From d1980c66be3d60b672884c8626b03150c6b5f6ea Mon Sep 17 00:00:00 2001 From: bmad4ever Date: Thu, 19 Oct 2023 21:37:22 +0100 Subject: [PATCH 03/59] Update custom-node-list.json renamed repo for ez import (no need for importlib) --- custom-node-list.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 9291b474..e9bd351c 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -753,10 +753,10 @@ }, { "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." From ae2456796070d308cc73ece0ed1d6831904d930a Mon Sep 17 00:00:00 2001 From: bmad4ever Date: Thu, 19 Oct 2023 21:39:43 +0100 Subject: [PATCH 04/59] Update extension-node-map.json --- extension-node-map.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extension-node-map.json b/extension-node-map.json index 1bbe2a4a..ccdaf1d7 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -1952,7 +1952,7 @@ "title_aux": "CLIPSeg" } ], - "https://github.com/bmad4ever/ComfyUI-Bmad-Custom-Nodes": [ + "https://github.com/bmad4ever/comfyui_bmad_nodes": [ [ "AdaptiveThresholding", "Add String To Many", @@ -2029,7 +2029,7 @@ "VAEEncodeBatch" ], { - "title_aux": "ComfyUI-Bmad-Custom-Nodes" + "title_aux": "Bmad Nodes" } ], "https://github.com/bradsec/ComfyUI_ResolutionSelector": [ @@ -3978,4 +3978,4 @@ "title_aux": "SDXLCustomAspectRatio" } ] -} \ No newline at end of file +} From ea1b5617faf244abaf938fed2b19690063678168 Mon Sep 17 00:00:00 2001 From: bmad4ever Date: Thu, 19 Oct 2023 21:46:22 +0100 Subject: [PATCH 05/59] Update custom-node-list.json --- custom-node-list.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom-node-list.json b/custom-node-list.json index e9bd351c..79700f72 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -759,7 +759,7 @@ "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", From d4d4baf7baed64ddc728828227979dcfeb44afd5 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Fri, 20 Oct 2023 23:38:37 +0900 Subject: [PATCH 06/59] update DB --- custom-node-list.json | 10 ++++++++++ extension-node-map.json | 23 ++++++++++++++++++++--- node_db/dev/custom-node-list.json | 10 ---------- node_db/new/custom-node-list.json | 10 ++++++++++ node_db/new/extension-node-map.json | 25 +++++++++++++++++++++---- 5 files changed, 61 insertions(+), 17 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 79700f72..8e27be78 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2264,6 +2264,16 @@ "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", + "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" + }, { "author": "taabata", "title": "Syrian Falcon Nodes", diff --git a/extension-node-map.json b/extension-node-map.json index ccdaf1d7..edc4a6aa 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -101,6 +101,7 @@ "BSZLatentDebug", "BSZLatentFill", "BSZLatentOffsetXL", + "BSZLatentRGBAImage", "BSZPixelbuster", "BSZPixelbusterHelp", "BSZPrincipledSDXL" @@ -407,8 +408,8 @@ ], "https://github.com/FizzleDorf/ComfyUI-AIT": [ [ - "AITLoader", - "AIT_Unet_Loader" + "AIT_Unet_Loader", + "AIT_VAE_Encode_Loader" ], { "title_aux": "ComfyUI-AIT" @@ -593,10 +594,12 @@ "VHS_LoadVideoPath", "VHS_MergeImages", "VHS_MergeLatents", + "VHS_OutVideoInfo", "VHS_SelectEveryNthImage", "VHS_SelectEveryNthLatent", "VHS_SplitImages", "VHS_SplitLatents", + "VHS_UploadVideo", "VHS_VideoCombine" ], { @@ -3297,6 +3300,19 @@ "title_aux": "SRL's nodes" } ], + "https://github.com/shadowcz007/comfyui-mixlab-nodes": [ + [ + "KandinskyModel", + "KandinskyModelLoad", + "LoadImageFromPath", + "RandomPrompt", + "SplitLongMask", + "TransparentImage" + ], + { + "title_aux": "comfyui-mixlab-nodes" + } + ], "https://github.com/shiimizu/ComfyUI_smZNodes": [ [ "smZ CLIPTextEncode", @@ -3853,6 +3869,7 @@ "Memory_Excel", "Model_1", "Ollama", + "Output2String", "Planner", "Scientist", "TextCombine", @@ -3978,4 +3995,4 @@ "title_aux": "SDXLCustomAspectRatio" } ] -} +} \ No newline at end of file diff --git a/node_db/dev/custom-node-list.json b/node_db/dev/custom-node-list.json index b06625a2..47b056ba 100644 --- a/node_db/dev/custom-node-list.json +++ b/node_db/dev/custom-node-list.json @@ -1,15 +1,5 @@ { "custom_nodes": [ - { - "author": "shadowcz007", - "title": "comfyui-mixlab-nodes", - "reference": "https://github.com/shadowcz007/comfyui-mixlab-nodes", - "files": [ - "https://github.com/shadowcz007/comfyui-mixlab-nodes" - ], - "install_type": "git-clone", - "description": "Nodes:RandomPrompt" - }, { "author": "AbyssYuan0", "title": "ComfyUI_BadgerTools", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 4c74bf2f..eee5ac1b 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "author": "shadowcz007", + "title": "comfyui-mixlab-nodes", + "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" + }, { "author": "chrisgoringe", "title": "Prompt Info", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 1bbe2a4a..edc4a6aa 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -101,6 +101,7 @@ "BSZLatentDebug", "BSZLatentFill", "BSZLatentOffsetXL", + "BSZLatentRGBAImage", "BSZPixelbuster", "BSZPixelbusterHelp", "BSZPrincipledSDXL" @@ -407,8 +408,8 @@ ], "https://github.com/FizzleDorf/ComfyUI-AIT": [ [ - "AITLoader", - "AIT_Unet_Loader" + "AIT_Unet_Loader", + "AIT_VAE_Encode_Loader" ], { "title_aux": "ComfyUI-AIT" @@ -593,10 +594,12 @@ "VHS_LoadVideoPath", "VHS_MergeImages", "VHS_MergeLatents", + "VHS_OutVideoInfo", "VHS_SelectEveryNthImage", "VHS_SelectEveryNthLatent", "VHS_SplitImages", "VHS_SplitLatents", + "VHS_UploadVideo", "VHS_VideoCombine" ], { @@ -1952,7 +1955,7 @@ "title_aux": "CLIPSeg" } ], - "https://github.com/bmad4ever/ComfyUI-Bmad-Custom-Nodes": [ + "https://github.com/bmad4ever/comfyui_bmad_nodes": [ [ "AdaptiveThresholding", "Add String To Many", @@ -2029,7 +2032,7 @@ "VAEEncodeBatch" ], { - "title_aux": "ComfyUI-Bmad-Custom-Nodes" + "title_aux": "Bmad Nodes" } ], "https://github.com/bradsec/ComfyUI_ResolutionSelector": [ @@ -3297,6 +3300,19 @@ "title_aux": "SRL's nodes" } ], + "https://github.com/shadowcz007/comfyui-mixlab-nodes": [ + [ + "KandinskyModel", + "KandinskyModelLoad", + "LoadImageFromPath", + "RandomPrompt", + "SplitLongMask", + "TransparentImage" + ], + { + "title_aux": "comfyui-mixlab-nodes" + } + ], "https://github.com/shiimizu/ComfyUI_smZNodes": [ [ "smZ CLIPTextEncode", @@ -3853,6 +3869,7 @@ "Memory_Excel", "Model_1", "Ollama", + "Output2String", "Planner", "Scientist", "TextCombine", From 82036c85e0e5a08956f0e2505c57550d0e070f69 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sat, 21 Oct 2023 08:33:17 +0900 Subject: [PATCH 07/59] update DB --- extension-node-map.json | 1 + node_db/new/extension-node-map.json | 1 + notebooks/comfyui_colab_with_manager.ipynb | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/extension-node-map.json b/extension-node-map.json index edc4a6aa..a6a8b155 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -2004,6 +2004,7 @@ "Get Models", "Get Prompt", "HypernetworkLoader (dirty)", + "ImageBatchToImageList", "InRange (hsv)", "Inpaint", "Input/String to Int Array", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index edc4a6aa..a6a8b155 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -2004,6 +2004,7 @@ "Get Models", "Get Prompt", "HypernetworkLoader (dirty)", + "ImageBatchToImageList", "InRange (hsv)", "Inpaint", "Input/String to Int Array", diff --git a/notebooks/comfyui_colab_with_manager.ipynb b/notebooks/comfyui_colab_with_manager.ipynb index 9320e9f9..5aeff075 100644 --- a/notebooks/comfyui_colab_with_manager.ipynb +++ b/notebooks/comfyui_colab_with_manager.ipynb @@ -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", From e4579869cbdc5568c0ec63305eb1548b9d2ac9fe Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sat, 21 Oct 2023 22:19:54 +0900 Subject: [PATCH 08/59] update DB --- custom-node-list.json | 10 ++++++++++ extension-node-map.json | 17 +++++++++++++++-- node_db/dev/custom-node-list.json | 10 ++++++++++ node_db/new/custom-node-list.json | 10 ++++++++++ node_db/new/extension-node-map.json | 17 +++++++++++++++-- 5 files changed, 60 insertions(+), 4 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 8e27be78..d1bfa956 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -447,6 +447,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", diff --git a/extension-node-map.json b/extension-node-map.json index a6a8b155..4d038350 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -104,7 +104,8 @@ "BSZLatentRGBAImage", "BSZPixelbuster", "BSZPixelbusterHelp", - "BSZPrincipledSDXL" + "BSZPrincipledSDXL", + "BSZPrincipledScale" ], { "title_aux": "bsz-cui-extras" @@ -633,6 +634,7 @@ ], "https://github.com/LucianoCirino/efficiency-nodes-comfyui": [ [ + "AnimateDiff Script", "Apply ControlNet Stack", "Control Net Stacker", "Eff. Loader SDXL", @@ -645,8 +647,9 @@ "KSampler SDXL (Eff.)", "LoRA Stacker", "Manual XY Entry Info", + "Noise Control Script", "Pack SDXL Tuple", - "Refiner Extras", + "Tiled Upscaler Script", "Unpack SDXL Tuple", "XY Input: Add/Return Noise", "XY Input: Aesthetic Score", @@ -2197,6 +2200,16 @@ "title_aux": "ComfyUI_DiT [WIP]" } ], + "https://github.com/city96/ComfyUI_ExtraModels": [ + [ + "DiTCondLabelEmpty", + "DiTCondLabelSelect", + "DitCheckpointLoader" + ], + { + "title_aux": "Extra Models for ComfyUI" + } + ], "https://github.com/city96/ComfyUI_NetDist": [ [ "FetchRemote", diff --git a/node_db/dev/custom-node-list.json b/node_db/dev/custom-node-list.json index 47b056ba..e466fab4 100644 --- a/node_db/dev/custom-node-list.json +++ b/node_db/dev/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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.

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.

" + }, { "author": "AbyssYuan0", "title": "ComfyUI_BadgerTools", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index eee5ac1b..9b2f05d6 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index a6a8b155..4d038350 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -104,7 +104,8 @@ "BSZLatentRGBAImage", "BSZPixelbuster", "BSZPixelbusterHelp", - "BSZPrincipledSDXL" + "BSZPrincipledSDXL", + "BSZPrincipledScale" ], { "title_aux": "bsz-cui-extras" @@ -633,6 +634,7 @@ ], "https://github.com/LucianoCirino/efficiency-nodes-comfyui": [ [ + "AnimateDiff Script", "Apply ControlNet Stack", "Control Net Stacker", "Eff. Loader SDXL", @@ -645,8 +647,9 @@ "KSampler SDXL (Eff.)", "LoRA Stacker", "Manual XY Entry Info", + "Noise Control Script", "Pack SDXL Tuple", - "Refiner Extras", + "Tiled Upscaler Script", "Unpack SDXL Tuple", "XY Input: Add/Return Noise", "XY Input: Aesthetic Score", @@ -2197,6 +2200,16 @@ "title_aux": "ComfyUI_DiT [WIP]" } ], + "https://github.com/city96/ComfyUI_ExtraModels": [ + [ + "DiTCondLabelEmpty", + "DiTCondLabelSelect", + "DitCheckpointLoader" + ], + { + "title_aux": "Extra Models for ComfyUI" + } + ], "https://github.com/city96/ComfyUI_NetDist": [ [ "FetchRemote", From ac6b2ee3e070683c747fb7589d27e2706a83dfa4 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 22 Oct 2023 22:51:21 +0900 Subject: [PATCH 09/59] update DB --- custom-node-list.json | 21 +++++++++++++++++++++ extension-node-map.json | 29 ++++++++++++++++++++++++++--- node_db/new/custom-node-list.json | 21 +++++++++++++++++++++ node_db/new/extension-node-map.json | 29 ++++++++++++++++++++++++++--- 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index d1bfa956..b9e02aee 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -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", @@ -2284,6 +2294,17 @@ "install_type": "git-clone", "description": "Nodes: RandomPrompt, TransparentImage, LoadImageFromPath, SplitLongMask" }, + { + "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": "taabata", "title": "Syrian Falcon Nodes", diff --git a/extension-node-map.json b/extension-node-map.json index 4d038350..c6d29fc0 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -367,6 +367,23 @@ "title_aux": "ComfyUI Loopchain" } ], + "https://github.com/Fannovel16/ComfyUI-MotionDiff": [ + [ + "EmptyMotionData", + "ExportSMPLTo3DSoftware", + "MotionCLIPTextEncode", + "MotionDataVisualizer", + "MotionDiffLoader", + "MotionDiffSimpleSampler", + "RenderSMPLMesh", + "SMPLLoader", + "SaveSMPL", + "SmplifyMotionData" + ], + { + "title_aux": "ComfyUI MotionDiff" + } + ], "https://github.com/Fannovel16/comfyui_controlnet_aux": [ [ "AIO_Preprocessor", @@ -595,12 +612,10 @@ "VHS_LoadVideoPath", "VHS_MergeImages", "VHS_MergeLatents", - "VHS_OutVideoInfo", "VHS_SelectEveryNthImage", "VHS_SelectEveryNthLatent", "VHS_SplitImages", "VHS_SplitLatents", - "VHS_UploadVideo", "VHS_VideoCombine" ], { @@ -979,8 +994,10 @@ "CR Color Bars", "CR Color Gradient", "CR Color Tint", + "CR Composite Text", "CR Conditioning Input Switch", "CR ControlNet Input Switch", + "CR Draw Text", "CR Float To Integer", "CR Float To String", "CR Halftone Grid", @@ -1005,6 +1022,7 @@ "CR LoRA Stack", "CR Load LoRA", "CR Load XY Annotation From File", + "CR Mask Text", "CR Model Input Switch", "CR Model Merge Stack", "CR Module Input", @@ -1012,6 +1030,7 @@ "CR Module Pipe Loader", "CR Multi Upscale Stack", "CR Multi-ControlNet Stack", + "CR Overlay Text", "CR Pipe Switch", "CR Polygons", "CR Process Switch", @@ -1025,6 +1044,8 @@ "CR Seed", "CR Seed to Int", "CR Split String", + "CR Starburst Colors", + "CR Starburst Lines", "CR String To Combo", "CR String To Number", "CR Style Bars", @@ -2204,7 +2225,8 @@ [ "DiTCondLabelEmpty", "DiTCondLabelSelect", - "DitCheckpointLoader" + "DitCheckpointLoader", + "ExtraVAELoader" ], { "title_aux": "Extra Models for ComfyUI" @@ -3709,6 +3731,7 @@ "ESS Number to X", "ESS Parameterizer", "ESS Parameterizer & Prompts", + "ESS Six Float Output", "ESS Six Input Random", "ESS Six Input Text Switch", "ESS Six Integer IO Switch", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 9b2f05d6..bce26eae 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,26 @@ { "custom_nodes": [ + { + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 4d038350..c6d29fc0 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -367,6 +367,23 @@ "title_aux": "ComfyUI Loopchain" } ], + "https://github.com/Fannovel16/ComfyUI-MotionDiff": [ + [ + "EmptyMotionData", + "ExportSMPLTo3DSoftware", + "MotionCLIPTextEncode", + "MotionDataVisualizer", + "MotionDiffLoader", + "MotionDiffSimpleSampler", + "RenderSMPLMesh", + "SMPLLoader", + "SaveSMPL", + "SmplifyMotionData" + ], + { + "title_aux": "ComfyUI MotionDiff" + } + ], "https://github.com/Fannovel16/comfyui_controlnet_aux": [ [ "AIO_Preprocessor", @@ -595,12 +612,10 @@ "VHS_LoadVideoPath", "VHS_MergeImages", "VHS_MergeLatents", - "VHS_OutVideoInfo", "VHS_SelectEveryNthImage", "VHS_SelectEveryNthLatent", "VHS_SplitImages", "VHS_SplitLatents", - "VHS_UploadVideo", "VHS_VideoCombine" ], { @@ -979,8 +994,10 @@ "CR Color Bars", "CR Color Gradient", "CR Color Tint", + "CR Composite Text", "CR Conditioning Input Switch", "CR ControlNet Input Switch", + "CR Draw Text", "CR Float To Integer", "CR Float To String", "CR Halftone Grid", @@ -1005,6 +1022,7 @@ "CR LoRA Stack", "CR Load LoRA", "CR Load XY Annotation From File", + "CR Mask Text", "CR Model Input Switch", "CR Model Merge Stack", "CR Module Input", @@ -1012,6 +1030,7 @@ "CR Module Pipe Loader", "CR Multi Upscale Stack", "CR Multi-ControlNet Stack", + "CR Overlay Text", "CR Pipe Switch", "CR Polygons", "CR Process Switch", @@ -1025,6 +1044,8 @@ "CR Seed", "CR Seed to Int", "CR Split String", + "CR Starburst Colors", + "CR Starburst Lines", "CR String To Combo", "CR String To Number", "CR Style Bars", @@ -2204,7 +2225,8 @@ [ "DiTCondLabelEmpty", "DiTCondLabelSelect", - "DitCheckpointLoader" + "DitCheckpointLoader", + "ExtraVAELoader" ], { "title_aux": "Extra Models for ComfyUI" @@ -3709,6 +3731,7 @@ "ESS Number to X", "ESS Parameterizer", "ESS Parameterizer & Prompts", + "ESS Six Float Output", "ESS Six Input Random", "ESS Six Input Text Switch", "ESS Six Integer IO Switch", From edbe6afa9c341a8c51c28b559315e86bf3a87c1e Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 22 Oct 2023 22:52:17 +0900 Subject: [PATCH 10/59] update DB --- node_db/new/custom-node-list.json | 168 ------------------------------ 1 file changed, 168 deletions(-) diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index bce26eae..ae73bc9b 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -914,174 +914,6 @@ ], "install_type": "git-clone", "description": "Nodes: ModelSamplerTonemapNoiseTest, ReferenceOnlySimple, RescaleClassifierFreeGuidanceTest, ModelMergeBlockNumber, ModelMergeSDXL, ModelMergeSDXLTransformers, ModelMergeSDXLDetailedTransformers.

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.

" - }, - { - "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 'FABRIC: Personalizing Diffusion Models with Iterative Feedback' (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.

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 here.

" - }, - { - "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." } ] } From d4ea4ae67e41eebbe339b17a3906f63c454bf3d2 Mon Sep 17 00:00:00 2001 From: "dr.lt.data" Date: Mon, 23 Oct 2023 12:32:17 +0900 Subject: [PATCH 11/59] update DB --- custom-node-list.json | 10 ---------- extension-node-map.json | 4 ++-- node_db/legacy/custom-node-list.json | 10 ++++++++++ node_db/new/extension-node-map.json | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index b9e02aee..9447f943 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -1744,16 +1744,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 model to use this node." - }, { "author": "spro", "title": "Latent Mirror node for ComfyUI", diff --git a/extension-node-map.json b/extension-node-map.json index c6d29fc0..33b71e37 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -1844,7 +1844,6 @@ "Text Input [Dream]", "Triangle Curve [Dream]", "Triangle Event Curve [Dream]", - "Video Encoder (mpegCoder) [Dream]", "WAV Curve [Dream]" ], { @@ -2894,7 +2893,8 @@ "TwoAdvancedSamplersForMask", "TwoSamplersForMask", "TwoSamplersForMaskUpscalerProvider", - "TwoSamplersForMaskUpscalerProviderPipe" + "TwoSamplersForMaskUpscalerProviderPipe", + "UltralyticsDetectorProvider" ], { "author": "Dr.Lt.Data", diff --git a/node_db/legacy/custom-node-list.json b/node_db/legacy/custom-node-list.json index c012a046..211b6a9b 100644 --- a/node_db/legacy/custom-node-list.json +++ b/node_db/legacy/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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 model to use this node. NOTE: This is removed." + }, { "author": "ProDALOR", "title": "comfyui_u2net", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index c6d29fc0..33b71e37 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -1844,7 +1844,6 @@ "Text Input [Dream]", "Triangle Curve [Dream]", "Triangle Event Curve [Dream]", - "Video Encoder (mpegCoder) [Dream]", "WAV Curve [Dream]" ], { @@ -2894,7 +2893,8 @@ "TwoAdvancedSamplersForMask", "TwoSamplersForMask", "TwoSamplersForMaskUpscalerProvider", - "TwoSamplersForMaskUpscalerProviderPipe" + "TwoSamplersForMaskUpscalerProviderPipe", + "UltralyticsDetectorProvider" ], { "author": "Dr.Lt.Data", From fa4d103d7fdab66c59c48bcad5aeae5876d87c9b Mon Sep 17 00:00:00 2001 From: 0xbitches <122321879+0xbitches@users.noreply.github.com> Date: Mon, 23 Oct 2023 06:06:16 -0700 Subject: [PATCH 12/59] Update custom-node-list.json --- custom-node-list.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/custom-node-list.json b/custom-node-list.json index 9447f943..42c6dc9f 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2284,6 +2284,16 @@ "install_type": "git-clone", "description": "Nodes: RandomPrompt, TransparentImage, LoadImageFromPath, SplitLongMask" }, + { + "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." + }, { "author": "ostris", "title": "Ostris Nodes ComfyUI", From 812a72aaf2ecc026b64682bb34b7f0e6e076723e Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Mon, 23 Oct 2023 22:33:57 +0900 Subject: [PATCH 13/59] update DB --- custom-node-list.json | 30 ++++++++++------ extension-node-map.json | 56 +++++++++++++++++------------ node_db/dev/custom-node-list.json | 12 ++++++- node_db/new/custom-node-list.json | 30 ++++++++++++++++ node_db/new/extension-node-map.json | 56 +++++++++++++++++------------ 5 files changed, 127 insertions(+), 57 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 42c6dc9f..e2fc772e 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2284,16 +2284,6 @@ "install_type": "git-clone", "description": "Nodes: RandomPrompt, TransparentImage, LoadImageFromPath, SplitLongMask" }, - { - "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." - }, { "author": "ostris", "title": "Ostris Nodes ComfyUI", @@ -2305,6 +2295,26 @@ "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." + }, + { + "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", diff --git a/extension-node-map.json b/extension-node-map.json index 33b71e37..44fdf4a5 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -9,6 +9,14 @@ "title_aux": "alkemann nodes" } ], + "https://github.com/0xbitches/ComfyUI-LCM": [ + [ + "LCMSampler" + ], + { + "title_aux": "Latent Consistency Model for ComfyUI" + } + ], "https://github.com/AIrjen/OneButtonPrompt": [ [ "CreatePromptVariant", @@ -102,6 +110,7 @@ "BSZLatentFill", "BSZLatentOffsetXL", "BSZLatentRGBAImage", + "BSZLatentbuster", "BSZPixelbuster", "BSZPixelbusterHelp", "BSZPrincipledSDXL", @@ -437,6 +446,7 @@ [ "AbsCosWave", "AbsSinWave", + "BatchGLIGENSchedule", "BatchPromptSchedule", "BatchPromptScheduleEncodeSDXL", "BatchValueSchedule", @@ -459,14 +469,6 @@ "title_aux": "FizzNodes" } ], - "https://github.com/GeLi1989/GK-beifen-ComfyUI_roop": [ - [ - "roop" - ], - { - "title_aux": "roop nodes for ComfyUI" - } - ], "https://github.com/Gourieff/comfyui-reactor-node": [ [ "ReActorFaceSwap" @@ -1258,21 +1260,21 @@ ], "https://github.com/THtianhao/ComfyUI-Portrait-Maker": [ [ - "BoxCropImage", - "ColorTransfer", - "ExpandMaskBox", - "FaceFusionPM", - "FaceSkin", - "GetImageInfo", - "ImageResizeTarget", - "ImageScaleShort", - "MaskDilateErode", - "MaskMerge2Image", - "PortraitEnhancement", - "RatioMerge2Image", - "ReplaceBoxImg", - "RetainFace", - "SkinRetouching" + "PM_BoxCropImage", + "PM_ColorTransfer", + "PM_ExpandMaskBox", + "PM_FaceFusion", + "PM_FaceSkin", + "PM_GetImageInfo", + "PM_ImageResizeTarget", + "PM_ImageScaleShort", + "PM_MaskDilateErode", + "PM_MaskMerge2Image", + "PM_PortraitEnhancement", + "PM_RatioMerge2Image", + "PM_ReplaceBoxImg", + "PM_RetinaFace", + "PM_SkinRetouching" ], { "title_aux": "ComfyUI-Portrait-Maker" @@ -1878,6 +1880,14 @@ "title_aux": "ComfyUI prompt control" } ], + "https://github.com/aszc-dev/ComfyUI-CoreMLSuite": [ + [ + "CoreMLUNetLoader" + ], + { + "title_aux": "Core ML Suite for ComfyUI" + } + ], "https://github.com/avatechai/avatar-graph-comfyui": [ [ "ApplyMeshTransformAsShapeKey", diff --git a/node_db/dev/custom-node-list.json b/node_db/dev/custom-node-list.json index e466fab4..6e3e147f 100644 --- a/node_db/dev/custom-node-list.json +++ b/node_db/dev/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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 https://github.com/0xbitches/sd-webui-lcm. 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.
NOTE: 0xbitches's 'Latent Consistency Model for ComfyUI' is original implementation." + }, { "author": "doucx", "title": "ComfyUI_WcpD_Utility_Kit", @@ -18,7 +28,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", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index ae73bc9b..e9ab5e4c 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,35 @@ { "custom_nodes": [ + { + "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." + }, + { + "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 https://github.com/0xbitches/sd-webui-lcm. 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.
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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 33b71e37..44fdf4a5 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -9,6 +9,14 @@ "title_aux": "alkemann nodes" } ], + "https://github.com/0xbitches/ComfyUI-LCM": [ + [ + "LCMSampler" + ], + { + "title_aux": "Latent Consistency Model for ComfyUI" + } + ], "https://github.com/AIrjen/OneButtonPrompt": [ [ "CreatePromptVariant", @@ -102,6 +110,7 @@ "BSZLatentFill", "BSZLatentOffsetXL", "BSZLatentRGBAImage", + "BSZLatentbuster", "BSZPixelbuster", "BSZPixelbusterHelp", "BSZPrincipledSDXL", @@ -437,6 +446,7 @@ [ "AbsCosWave", "AbsSinWave", + "BatchGLIGENSchedule", "BatchPromptSchedule", "BatchPromptScheduleEncodeSDXL", "BatchValueSchedule", @@ -459,14 +469,6 @@ "title_aux": "FizzNodes" } ], - "https://github.com/GeLi1989/GK-beifen-ComfyUI_roop": [ - [ - "roop" - ], - { - "title_aux": "roop nodes for ComfyUI" - } - ], "https://github.com/Gourieff/comfyui-reactor-node": [ [ "ReActorFaceSwap" @@ -1258,21 +1260,21 @@ ], "https://github.com/THtianhao/ComfyUI-Portrait-Maker": [ [ - "BoxCropImage", - "ColorTransfer", - "ExpandMaskBox", - "FaceFusionPM", - "FaceSkin", - "GetImageInfo", - "ImageResizeTarget", - "ImageScaleShort", - "MaskDilateErode", - "MaskMerge2Image", - "PortraitEnhancement", - "RatioMerge2Image", - "ReplaceBoxImg", - "RetainFace", - "SkinRetouching" + "PM_BoxCropImage", + "PM_ColorTransfer", + "PM_ExpandMaskBox", + "PM_FaceFusion", + "PM_FaceSkin", + "PM_GetImageInfo", + "PM_ImageResizeTarget", + "PM_ImageScaleShort", + "PM_MaskDilateErode", + "PM_MaskMerge2Image", + "PM_PortraitEnhancement", + "PM_RatioMerge2Image", + "PM_ReplaceBoxImg", + "PM_RetinaFace", + "PM_SkinRetouching" ], { "title_aux": "ComfyUI-Portrait-Maker" @@ -1878,6 +1880,14 @@ "title_aux": "ComfyUI prompt control" } ], + "https://github.com/aszc-dev/ComfyUI-CoreMLSuite": [ + [ + "CoreMLUNetLoader" + ], + { + "title_aux": "Core ML Suite for ComfyUI" + } + ], "https://github.com/avatechai/avatar-graph-comfyui": [ [ "ApplyMeshTransformAsShapeKey", From 67c28ee3da550551315b1243bc5009eb67720226 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Tue, 24 Oct 2023 19:20:41 +0900 Subject: [PATCH 14/59] update DB --- extension-node-map.json | 2 ++ node_db/dev/custom-node-list.json | 10 ++++++++++ node_db/new/extension-node-map.json | 2 ++ 3 files changed, 14 insertions(+) diff --git a/extension-node-map.json b/extension-node-map.json index 44fdf4a5..1fa454b2 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -106,8 +106,10 @@ "BSZAspectHires", "BSZColoredLatentImageXL", "BSZCombinedHires", + "BSZInjectionKSampler", "BSZLatentDebug", "BSZLatentFill", + "BSZLatentGradient", "BSZLatentOffsetXL", "BSZLatentRGBAImage", "BSZLatentbuster", diff --git a/node_db/dev/custom-node-list.json b/node_db/dev/custom-node-list.json index 6e3e147f..1a98ad11 100644 --- a/node_db/dev/custom-node-list.json +++ b/node_db/dev/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 44fdf4a5..1fa454b2 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -106,8 +106,10 @@ "BSZAspectHires", "BSZColoredLatentImageXL", "BSZCombinedHires", + "BSZInjectionKSampler", "BSZLatentDebug", "BSZLatentFill", + "BSZLatentGradient", "BSZLatentOffsetXL", "BSZLatentRGBAImage", "BSZLatentbuster", From 38190038346aad47504d57bede39aeb6c066d80e Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Wed, 25 Oct 2023 00:58:19 +0900 Subject: [PATCH 15/59] update DB --- extension-node-map.json | 7 ++++++- node_db/new/extension-node-map.json | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/extension-node-map.json b/extension-node-map.json index 1fa454b2..9a4b65b8 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -11,7 +11,8 @@ ], "https://github.com/0xbitches/ComfyUI-LCM": [ [ - "LCMSampler" + "LCM_Sampler", + "LCM_img2img_Sampler" ], { "title_aux": "Latent Consistency Model for ComfyUI" @@ -1266,10 +1267,12 @@ "PM_ColorTransfer", "PM_ExpandMaskBox", "PM_FaceFusion", + "PM_FaceShapMatch", "PM_FaceSkin", "PM_GetImageInfo", "PM_ImageResizeTarget", "PM_ImageScaleShort", + "PM_MakeUpTransfer", "PM_MaskDilateErode", "PM_MaskMerge2Image", "PM_PortraitEnhancement", @@ -1288,6 +1291,7 @@ "tri3d-atr-parse-batch", "tri3d-extract-hand", "tri3d-extract-parts-batch", + "tri3d-extract-parts-mask-batch", "tri3d-fuzzification", "tri3d-position-hands", "tri3d-position-parts-batch" @@ -2781,6 +2785,7 @@ "CLIPSegDetectorProvider", "CfgScheduleHookProvider", "CombineRegionalPrompts", + "CoreMLDetailerHookProvider", "DenoiseScheduleHookProvider", "DetailerForEach", "DetailerForEachDebug", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 1fa454b2..9a4b65b8 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -11,7 +11,8 @@ ], "https://github.com/0xbitches/ComfyUI-LCM": [ [ - "LCMSampler" + "LCM_Sampler", + "LCM_img2img_Sampler" ], { "title_aux": "Latent Consistency Model for ComfyUI" @@ -1266,10 +1267,12 @@ "PM_ColorTransfer", "PM_ExpandMaskBox", "PM_FaceFusion", + "PM_FaceShapMatch", "PM_FaceSkin", "PM_GetImageInfo", "PM_ImageResizeTarget", "PM_ImageScaleShort", + "PM_MakeUpTransfer", "PM_MaskDilateErode", "PM_MaskMerge2Image", "PM_PortraitEnhancement", @@ -1288,6 +1291,7 @@ "tri3d-atr-parse-batch", "tri3d-extract-hand", "tri3d-extract-parts-batch", + "tri3d-extract-parts-mask-batch", "tri3d-fuzzification", "tri3d-position-hands", "tri3d-position-parts-batch" @@ -2781,6 +2785,7 @@ "CLIPSegDetectorProvider", "CfgScheduleHookProvider", "CombineRegionalPrompts", + "CoreMLDetailerHookProvider", "DenoiseScheduleHookProvider", "DetailerForEach", "DetailerForEachDebug", From 7de953aab8c0cf3ca273a958bdcef18a05f013bb Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Wed, 25 Oct 2023 08:40:34 +0900 Subject: [PATCH 16/59] update DB --- custom-node-list.json | 10 ++++++++++ extension-node-map.json | 12 ++++++++++++ node_db/new/custom-node-list.json | 10 ++++++++++ node_db/new/extension-node-map.json | 12 ++++++++++++ 4 files changed, 44 insertions(+) diff --git a/custom-node-list.json b/custom-node-list.json index e2fc772e..29f12a96 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2325,6 +2325,16 @@ "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": "Ser-Hilary", "title": "SDXL_sizing", diff --git a/extension-node-map.json b/extension-node-map.json index 9a4b65b8..1c493e83 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -3658,6 +3658,18 @@ "title_aux": "Syrian Falcon Nodes" } ], + "https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy": [ + [ + "LCMGenerate", + "LCMLoader", + "LCM_outpaint_prep", + "LoadImageNode_LCM", + "SaveImage_LCM" + ], + { + "title_aux": "LCM_Inpaint-Outpaint_Comfy" + } + ], "https://github.com/theUpsider/ComfyUI-Logic": [ [ "Compare", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index e9ab5e4c..f8996fb5 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 9a4b65b8..1c493e83 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -3658,6 +3658,18 @@ "title_aux": "Syrian Falcon Nodes" } ], + "https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy": [ + [ + "LCMGenerate", + "LCMLoader", + "LCM_outpaint_prep", + "LoadImageNode_LCM", + "SaveImage_LCM" + ], + { + "title_aux": "LCM_Inpaint-Outpaint_Comfy" + } + ], "https://github.com/theUpsider/ComfyUI-Logic": [ [ "Compare", From a677a286bff5725b8b07cd11ff27aafafecdd303 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Wed, 25 Oct 2023 19:38:34 +0900 Subject: [PATCH 17/59] update DB --- custom-node-list.json | 4 ++-- extension-node-map.json | 11 +++++++++-- node_db/new/custom-node-list.json | 4 ++-- node_db/new/extension-node-map.json | 11 +++++++++-- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 29f12a96..935877bb 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2276,13 +2276,13 @@ }, { "author": "shadowcz007", - "title": "comfyui-mixlab-nodes", + "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" + "description": "Nodes: RandomPrompt, TransparentImage, LoadImageFromPath, SplitLongMask, ImagesCrop, RunWorkflow" }, { "author": "ostris", diff --git a/extension-node-map.json b/extension-node-map.json index 1c493e83..3870043d 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -1757,6 +1757,7 @@ "GateNormalizedAmplitude", "LoadAudio", "NormalizeAmplitude", + "NormalizedAmplitudeDrivenString", "NormalizedAmplitudeToGraph", "NormalizedAmplitudeToNumber", "TransientAmplitudeBasic" @@ -1940,6 +1941,7 @@ "Batch Crop Resize Inplace", "Batch Load Images", "Batch Resize Image for SDXL", + "Checkpoint Loader Simple Mikey", "Empty Latent Ratio Custom SDXL", "Empty Latent Ratio Select SDXL", "FileNamePrefix", @@ -1956,6 +1958,7 @@ "Mikey Sampler Tiled", "Mikey Sampler Tiled Base Only", "MikeySamplerTiledAdvanced", + "OoobaPrompt", "PresetRatioSelector", "Prompt With SDXL", "Prompt With Style", @@ -1977,6 +1980,7 @@ "Text2InputOr3rdOption", "TextCombinations", "TextCombinations3", + "TextPreserve", "Upscale Tile Calculator", "Wildcard Processor", "WildcardAndLoraSyntaxProcessor" @@ -3355,15 +3359,17 @@ ], "https://github.com/shadowcz007/comfyui-mixlab-nodes": [ [ + "ImagesCrop", "KandinskyModel", "KandinskyModelLoad", - "LoadImageFromPath", + "LoadImagesFromPath", "RandomPrompt", + "RunWorkflow", "SplitLongMask", "TransparentImage" ], { - "title_aux": "comfyui-mixlab-nodes" + "title_aux": "comfyui-mixlab-nodes [WIP]" } ], "https://github.com/shiimizu/ComfyUI_smZNodes": [ @@ -3425,6 +3431,7 @@ "ColorCorrect", "DeepDanbooruCaption", "DependenciesEdit", + "FaceAnalyze", "Fooocus_KSampler", "Fooocus_KSamplerAdvanced", "GetBoolFromJson", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index f8996fb5..0067c674 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -73,13 +73,13 @@ }, { "author": "shadowcz007", - "title": "comfyui-mixlab-nodes", + "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" + "description": "Nodes: RandomPrompt, TransparentImage, LoadImageFromPath, SplitLongMask, ImagesCrop, RunWorkflow" }, { "author": "chrisgoringe", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 1c493e83..3870043d 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -1757,6 +1757,7 @@ "GateNormalizedAmplitude", "LoadAudio", "NormalizeAmplitude", + "NormalizedAmplitudeDrivenString", "NormalizedAmplitudeToGraph", "NormalizedAmplitudeToNumber", "TransientAmplitudeBasic" @@ -1940,6 +1941,7 @@ "Batch Crop Resize Inplace", "Batch Load Images", "Batch Resize Image for SDXL", + "Checkpoint Loader Simple Mikey", "Empty Latent Ratio Custom SDXL", "Empty Latent Ratio Select SDXL", "FileNamePrefix", @@ -1956,6 +1958,7 @@ "Mikey Sampler Tiled", "Mikey Sampler Tiled Base Only", "MikeySamplerTiledAdvanced", + "OoobaPrompt", "PresetRatioSelector", "Prompt With SDXL", "Prompt With Style", @@ -1977,6 +1980,7 @@ "Text2InputOr3rdOption", "TextCombinations", "TextCombinations3", + "TextPreserve", "Upscale Tile Calculator", "Wildcard Processor", "WildcardAndLoraSyntaxProcessor" @@ -3355,15 +3359,17 @@ ], "https://github.com/shadowcz007/comfyui-mixlab-nodes": [ [ + "ImagesCrop", "KandinskyModel", "KandinskyModelLoad", - "LoadImageFromPath", + "LoadImagesFromPath", "RandomPrompt", + "RunWorkflow", "SplitLongMask", "TransparentImage" ], { - "title_aux": "comfyui-mixlab-nodes" + "title_aux": "comfyui-mixlab-nodes [WIP]" } ], "https://github.com/shiimizu/ComfyUI_smZNodes": [ @@ -3425,6 +3431,7 @@ "ColorCorrect", "DeepDanbooruCaption", "DependenciesEdit", + "FaceAnalyze", "Fooocus_KSampler", "Fooocus_KSamplerAdvanced", "GetBoolFromJson", From 848bb0c826559c2664fec93a76515c9341bd3429 Mon Sep 17 00:00:00 2001 From: morphicschris Date: Thu, 26 Oct 2023 09:16:38 +0100 Subject: [PATCH 18/59] Changed ImageGlitcher custom node location to new address which includes extra nodes --- custom-node-list.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 935877bb..43614360 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2186,13 +2186,13 @@ }, { "author": "chrish-slingshot", - "title": "ComfyUI-ImageGlitcher", - "reference": "https://github.com/chrish-slingshot/ComfyUI-ImageGlitcher", + "title": "CrasH Utils", + "reference": "https://github.com/chrish-slingshot/ComfyUI-CrasHUtils", "files": [ "https://github.com/chrish-slingshot/ComfyUI-ImageGlitcher" ], "install_type": "git-clone", - "description": "Nodes: ImageGlitcher. Based on the HTML image glitcher by Felix Turner here." + "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", From 2cf81dec9ec5dd1f3cfd454aee8064d971102c01 Mon Sep 17 00:00:00 2001 From: ltdrdata Date: Thu, 26 Oct 2023 21:55:53 +0900 Subject: [PATCH 19/59] update DB --- custom-node-list.json | 4 ++-- extension-node-map.json | 19 +++++++++---------- node_db/new/custom-node-list.json | 8 ++++---- node_db/new/extension-node-map.json | 19 +++++++++---------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 43614360..f4fb7871 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2187,9 +2187,9 @@ { "author": "chrish-slingshot", "title": "CrasH Utils", - "reference": "https://github.com/chrish-slingshot/ComfyUI-CrasHUtils", + "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": "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). " diff --git a/extension-node-map.json b/extension-node-map.json index 3870043d..1bdb2f84 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -12,7 +12,9 @@ "https://github.com/0xbitches/ComfyUI-LCM": [ [ "LCM_Sampler", - "LCM_img2img_Sampler" + "LCM_Sampler_Advanced", + "LCM_img2img_Sampler", + "LCM_img2img_Sampler_Advanced" ], { "title_aux": "Latent Consistency Model for ComfyUI" @@ -111,6 +113,7 @@ "BSZLatentDebug", "BSZLatentFill", "BSZLatentGradient", + "BSZLatentHSVAImage", "BSZLatentOffsetXL", "BSZLatentRGBAImage", "BSZLatentbuster", @@ -452,6 +455,7 @@ "BatchGLIGENSchedule", "BatchPromptSchedule", "BatchPromptScheduleEncodeSDXL", + "BatchStringSchedule", "BatchValueSchedule", "CosWave", "InvCosWave", @@ -1279,7 +1283,9 @@ "PM_RatioMerge2Image", "PM_ReplaceBoxImg", "PM_RetinaFace", - "PM_SkinRetouching" + "PM_SkinRetouching", + "PM_SuperColorTransfer", + "PM_SuperMakeUpTransfer" ], { "title_aux": "ComfyUI-Portrait-Maker" @@ -2208,14 +2214,6 @@ "title_aux": "Use Everywhere (UE Nodes)" } ], - "https://github.com/chrish-slingshot/ComfyUI-ImageGlitcher": [ - [ - "ImageGlitcher" - ], - { - "title_aux": "ComfyUI-ImageGlitcher" - } - ], "https://github.com/city96/ComfyUI_ColorMod": [ [ "ColorModEdges", @@ -3829,6 +3827,7 @@ "KSamplerAdvanced (WLSH)", "Multiply Integer (WLSH)", "Outpaint to Image (WLSH)", + "Prompt Weight (WLSH)", "Quick Resolution Multiply (WLSH)", "Resolutions by Ratio (WLSH)", "SDXL Quick Empty Latent (WLSH)", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 0067c674..4cd5c517 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -203,13 +203,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 here." + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 3870043d..1bdb2f84 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -12,7 +12,9 @@ "https://github.com/0xbitches/ComfyUI-LCM": [ [ "LCM_Sampler", - "LCM_img2img_Sampler" + "LCM_Sampler_Advanced", + "LCM_img2img_Sampler", + "LCM_img2img_Sampler_Advanced" ], { "title_aux": "Latent Consistency Model for ComfyUI" @@ -111,6 +113,7 @@ "BSZLatentDebug", "BSZLatentFill", "BSZLatentGradient", + "BSZLatentHSVAImage", "BSZLatentOffsetXL", "BSZLatentRGBAImage", "BSZLatentbuster", @@ -452,6 +455,7 @@ "BatchGLIGENSchedule", "BatchPromptSchedule", "BatchPromptScheduleEncodeSDXL", + "BatchStringSchedule", "BatchValueSchedule", "CosWave", "InvCosWave", @@ -1279,7 +1283,9 @@ "PM_RatioMerge2Image", "PM_ReplaceBoxImg", "PM_RetinaFace", - "PM_SkinRetouching" + "PM_SkinRetouching", + "PM_SuperColorTransfer", + "PM_SuperMakeUpTransfer" ], { "title_aux": "ComfyUI-Portrait-Maker" @@ -2208,14 +2214,6 @@ "title_aux": "Use Everywhere (UE Nodes)" } ], - "https://github.com/chrish-slingshot/ComfyUI-ImageGlitcher": [ - [ - "ImageGlitcher" - ], - { - "title_aux": "ComfyUI-ImageGlitcher" - } - ], "https://github.com/city96/ComfyUI_ColorMod": [ [ "ColorModEdges", @@ -3829,6 +3827,7 @@ "KSamplerAdvanced (WLSH)", "Multiply Integer (WLSH)", "Outpaint to Image (WLSH)", + "Prompt Weight (WLSH)", "Quick Resolution Multiply (WLSH)", "Resolutions by Ratio (WLSH)", "SDXL Quick Empty Latent (WLSH)", From eab06ea1a9a1c9c8907886e0845f85ea6eb22e25 Mon Sep 17 00:00:00 2001 From: Ape* <75503067+GMapeSplat@users.noreply.github.com> Date: Fri, 27 Oct 2023 17:49:23 -0500 Subject: [PATCH 20/59] Update custom-node-list.json Added ezXY to node-list. repo: https://github.com/GMapeSplat/ComfyUI_ezXY --- custom-node-list.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/custom-node-list.json b/custom-node-list.json index f4fb7871..7898403a 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2315,6 +2315,16 @@ "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": "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.

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.

Nodes: A collection of nodes for facilitating the generation of XY plots. Capable of plotting changes over most primitive values." + }, { "author": "taabata", "title": "Syrian Falcon Nodes", From cb7ac4fb4172ec7bb834e4e1ea4e4690afbc1b4a Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Fri, 27 Oct 2023 14:33:28 +0900 Subject: [PATCH 21/59] update DB --- custom-node-list.json | 30 +++++++++++++++++++++++ extension-node-map.json | 38 +++++++++++++++++++++++++++++ node_db/new/custom-node-list.json | 30 +++++++++++++++++++++++ node_db/new/extension-node-map.json | 38 +++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/custom-node-list.json b/custom-node-list.json index 7898403a..dc61b2f5 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2345,6 +2345,26 @@ "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": "GMapeSplat", + "title": "ezXY", + "reference": "https://github.com/GMapeSplat/ComfyUI_ezXY", + "files": [ + "https://github.com/GMapeSplat/ComfyUI_ezXY" + ], + "install_type": "git-clone", + "description": "Features: Automatic type-casting for most custom and native nodes, Compact math node, List generation for iteration control, Versitile XY plotter" + }, { "author": "Ser-Hilary", "title": "SDXL_sizing", @@ -2563,6 +2583,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", diff --git a/extension-node-map.json b/extension-node-map.json index 1bdb2f84..0a55a2c7 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -193,6 +193,15 @@ "title_aux": "Tiled sampling for ComfyUI" } ], + "https://github.com/CaptainGrock/ComfyUIInvisibleWatermark/raw/main/Invisible%20Watermark.py": [ + [ + "Apply Invisible Watermark", + "Extract Watermark" + ], + { + "title_aux": "ComfyUIInvisibleWatermark" + } + ], "https://github.com/Chaoses-Ib/ComfyUI_Ib_CustomNodes": [ [ "LoadImageFromPath" @@ -476,6 +485,21 @@ "title_aux": "FizzNodes" } ], + "https://github.com/GMapeSplat/ComfyUI_ezXY": [ + [ + "ItemFromList", + "IterationDriver", + "LineToConsole", + "NumbersToList", + "PlotImages", + "StringToList", + "ezMath", + "ezXY_Driver" + ], + { + "title_aux": "ezXY" + } + ], "https://github.com/Gourieff/comfyui-reactor-node": [ [ "ReActorFaceSwap" @@ -3151,6 +3175,17 @@ "title_aux": "comfyUI_Nodes_nicolai256" } ], + "https://github.com/noxinias/ComfyUI_NoxinNodes": [ + [ + "NOXCHIME", + "NOXPROMPTLIB_LOAD", + "NOXPROMPTLIB_SAVE", + "NOXSCALEDRES" + ], + { + "title_aux": "ComfyUI_NoxinNodes" + } + ], "https://github.com/ntdviet/comfyui-ext/raw/main/custom_nodes/gcLatentTunnel/gcLatentTunnel.py": [ [ "gcLatentTunnel" @@ -3665,8 +3700,11 @@ ], "https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy": [ [ + "FreeU_LCM", "LCMGenerate", + "LCMGenerate_img2img", "LCMLoader", + "LCMLoader_img2img", "LCM_outpaint_prep", "LoadImageNode_LCM", "SaveImage_LCM" diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 4cd5c517..7350f90b 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,35 @@ { "custom_nodes": [ + { + "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": "GMapeSplat", + "title": "ezXY", + "reference": "https://github.com/GMapeSplat/ComfyUI_ezXY", + "files": [ + "https://github.com/GMapeSplat/ComfyUI_ezXY" + ], + "install_type": "git-clone", + "description": "Features: Automatic type-casting for most custom and native nodes, Compact math node, List generation for iteration control, Versitile XY plotter" + }, + { + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 1bdb2f84..0a55a2c7 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -193,6 +193,15 @@ "title_aux": "Tiled sampling for ComfyUI" } ], + "https://github.com/CaptainGrock/ComfyUIInvisibleWatermark/raw/main/Invisible%20Watermark.py": [ + [ + "Apply Invisible Watermark", + "Extract Watermark" + ], + { + "title_aux": "ComfyUIInvisibleWatermark" + } + ], "https://github.com/Chaoses-Ib/ComfyUI_Ib_CustomNodes": [ [ "LoadImageFromPath" @@ -476,6 +485,21 @@ "title_aux": "FizzNodes" } ], + "https://github.com/GMapeSplat/ComfyUI_ezXY": [ + [ + "ItemFromList", + "IterationDriver", + "LineToConsole", + "NumbersToList", + "PlotImages", + "StringToList", + "ezMath", + "ezXY_Driver" + ], + { + "title_aux": "ezXY" + } + ], "https://github.com/Gourieff/comfyui-reactor-node": [ [ "ReActorFaceSwap" @@ -3151,6 +3175,17 @@ "title_aux": "comfyUI_Nodes_nicolai256" } ], + "https://github.com/noxinias/ComfyUI_NoxinNodes": [ + [ + "NOXCHIME", + "NOXPROMPTLIB_LOAD", + "NOXPROMPTLIB_SAVE", + "NOXSCALEDRES" + ], + { + "title_aux": "ComfyUI_NoxinNodes" + } + ], "https://github.com/ntdviet/comfyui-ext/raw/main/custom_nodes/gcLatentTunnel/gcLatentTunnel.py": [ [ "gcLatentTunnel" @@ -3665,8 +3700,11 @@ ], "https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy": [ [ + "FreeU_LCM", "LCMGenerate", + "LCMGenerate_img2img", "LCMLoader", + "LCMLoader_img2img", "LCM_outpaint_prep", "LoadImageNode_LCM", "SaveImage_LCM" From e776a18772edb6f7ce66f18fe31142141e30e5f9 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sat, 28 Oct 2023 08:30:02 +0900 Subject: [PATCH 22/59] update DB --- custom-node-list.json | 16 +++------------- extension-node-map.json | 22 +++++++++++++++------- node_db/dev/custom-node-list.json | 10 ++++++++++ node_db/new/custom-node-list.json | 6 +++--- node_db/new/extension-node-map.json | 22 +++++++++++++++------- 5 files changed, 46 insertions(+), 30 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index dc61b2f5..1432e117 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2315,16 +2315,6 @@ "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": "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.

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.

Nodes: A collection of nodes for facilitating the generation of XY plots. Capable of plotting changes over most primitive values." - }, { "author": "taabata", "title": "Syrian Falcon Nodes", @@ -2356,14 +2346,14 @@ "description": "Nodes: Noxin Complete Chime, Noxin Scaled Resolutions, Load from Noxin Prompt Library, Save to Noxin Prompt Library" }, { - "author": "GMapeSplat", - "title": "ezXY", + "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": "Features: Automatic type-casting for most custom and native nodes, Compact math node, List generation for iteration control, Versitile XY plotter" + "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.

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.

Nodes: A collection of nodes for facilitating the generation of XY plots. Capable of plotting changes over most primitive values." }, { "author": "Ser-Hilary", diff --git a/extension-node-map.json b/extension-node-map.json index 0a55a2c7..e162a739 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -119,7 +119,7 @@ "BSZLatentbuster", "BSZPixelbuster", "BSZPixelbusterHelp", - "BSZPrincipledSDXL", + "BSZPrincipledSampler", "BSZPrincipledScale" ], { @@ -497,7 +497,7 @@ "ezXY_Driver" ], { - "title_aux": "ezXY" + "title_aux": "ezXY scripts and nodes" } ], "https://github.com/Gourieff/comfyui-reactor-node": [ @@ -2267,7 +2267,13 @@ "DiTCondLabelEmpty", "DiTCondLabelSelect", "DitCheckpointLoader", - "ExtraVAELoader" + "ExtraVAELoader", + "PixArtCheckpointLoader", + "PixArtDPMSampler", + "PixArtResolutionSelect", + "PixArtT5TextEncode", + "T5TextEncode", + "T5v11Loader" ], { "title_aux": "Extra Models for ComfyUI" @@ -2694,11 +2700,13 @@ "ConditioningSetMaskAndCombine", "ConditioningSetMaskAndCombine3", "ConditioningSetMaskAndCombine4", + "ConditioningSetMaskAndCombine5", "CreateAudioMask", "CreateFadeMask", "CreateFluidMask", "CreateGradientMask", "CreateTextMask", + "CrossFadeImages", "GrowMaskWithBlur", "INTConstant", "VRAM_Debug" @@ -3177,10 +3185,10 @@ ], "https://github.com/noxinias/ComfyUI_NoxinNodes": [ [ - "NOXCHIME", - "NOXPROMPTLIB_LOAD", - "NOXPROMPTLIB_SAVE", - "NOXSCALEDRES" + "NoxinChime", + "NoxinPromptLoad", + "NoxinPromptSave", + "NoxinScaledResolution" ], { "title_aux": "ComfyUI_NoxinNodes" diff --git a/node_db/dev/custom-node-list.json b/node_db/dev/custom-node-list.json index 1a98ad11..b12fbd3d 100644 --- a/node_db/dev/custom-node-list.json +++ b/node_db/dev/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 7350f90b..678beb61 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -11,14 +11,14 @@ "description": "Nodes:Apply Invisible Watermark, Extract Watermark. Adds up to 12 characters encoded into an image that can be extracted." }, { - "author": "GMapeSplat", - "title": "ezXY", + "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": "Features: Automatic type-casting for most custom and native nodes, Compact math node, List generation for iteration control, Versitile XY plotter" + "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.

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.

Nodes: A collection of nodes for facilitating the generation of XY plots. Capable of plotting changes over most primitive values." }, { "author": "noxinias", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 0a55a2c7..e162a739 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -119,7 +119,7 @@ "BSZLatentbuster", "BSZPixelbuster", "BSZPixelbusterHelp", - "BSZPrincipledSDXL", + "BSZPrincipledSampler", "BSZPrincipledScale" ], { @@ -497,7 +497,7 @@ "ezXY_Driver" ], { - "title_aux": "ezXY" + "title_aux": "ezXY scripts and nodes" } ], "https://github.com/Gourieff/comfyui-reactor-node": [ @@ -2267,7 +2267,13 @@ "DiTCondLabelEmpty", "DiTCondLabelSelect", "DitCheckpointLoader", - "ExtraVAELoader" + "ExtraVAELoader", + "PixArtCheckpointLoader", + "PixArtDPMSampler", + "PixArtResolutionSelect", + "PixArtT5TextEncode", + "T5TextEncode", + "T5v11Loader" ], { "title_aux": "Extra Models for ComfyUI" @@ -2694,11 +2700,13 @@ "ConditioningSetMaskAndCombine", "ConditioningSetMaskAndCombine3", "ConditioningSetMaskAndCombine4", + "ConditioningSetMaskAndCombine5", "CreateAudioMask", "CreateFadeMask", "CreateFluidMask", "CreateGradientMask", "CreateTextMask", + "CrossFadeImages", "GrowMaskWithBlur", "INTConstant", "VRAM_Debug" @@ -3177,10 +3185,10 @@ ], "https://github.com/noxinias/ComfyUI_NoxinNodes": [ [ - "NOXCHIME", - "NOXPROMPTLIB_LOAD", - "NOXPROMPTLIB_SAVE", - "NOXSCALEDRES" + "NoxinChime", + "NoxinPromptLoad", + "NoxinPromptSave", + "NoxinScaledResolution" ], { "title_aux": "ComfyUI_NoxinNodes" From 01f89a7df727c32a4a1a9f216a2544c266cfa647 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 29 Oct 2023 11:18:25 +0900 Subject: [PATCH 23/59] update DB --- custom-node-list.json | 10 +++ extension-node-map.json | 30 +++++++- node_db/new/custom-node-list.json | 110 +++------------------------- node_db/new/extension-node-map.json | 30 +++++++- 4 files changed, 74 insertions(+), 106 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 1432e117..4208d974 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2355,6 +2355,16 @@ "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.

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.

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": "Ser-Hilary", "title": "SDXL_sizing", diff --git a/extension-node-map.json b/extension-node-map.json index e162a739..44cf0dc5 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -119,6 +119,7 @@ "BSZLatentbuster", "BSZPixelbuster", "BSZPixelbusterHelp", + "BSZPrincipledConditioning", "BSZPrincipledSampler", "BSZPrincipledScale" ], @@ -464,8 +465,11 @@ "BatchGLIGENSchedule", "BatchPromptSchedule", "BatchPromptScheduleEncodeSDXL", + "BatchPromptScheduleLatentInput", + "BatchPromptScheduleSDXLLatentInput", "BatchStringSchedule", "BatchValueSchedule", + "BatchValueScheduleLatentInput", "CosWave", "InvCosWave", "InvSinWave", @@ -1030,6 +1034,7 @@ "CR Composite Text", "CR Conditioning Input Switch", "CR ControlNet Input Switch", + "CR Draw Perspective Text", "CR Draw Text", "CR Float To Integer", "CR Float To String", @@ -1076,6 +1081,7 @@ "CR SDXL Style Text", "CR Seed", "CR Seed to Int", + "CR Simple Meme Template", "CR Split String", "CR Starburst Colors", "CR Starburst Lines", @@ -1988,7 +1994,7 @@ "Mikey Sampler Tiled", "Mikey Sampler Tiled Base Only", "MikeySamplerTiledAdvanced", - "OoobaPrompt", + "OobaPrompt", "PresetRatioSelector", "Prompt With SDXL", "Prompt With Style", @@ -2202,9 +2208,13 @@ ], "https://github.com/chibiace/ComfyUI-Chibi-Nodes": [ [ + "ConditionText", "ImageTool", + "LoadEmbedding", "Loader", - "Prompts" + "Prompts", + "SaveImages", + "Wildcards" ], { "title_aux": "ComfyUI-Chibi-Nodes" @@ -2709,12 +2719,22 @@ "CrossFadeImages", "GrowMaskWithBlur", "INTConstant", + "SomethingToString", "VRAM_Debug" ], { "title_aux": "KJNodes for ComfyUI" } ], + "https://github.com/kinfolk0117/ComfyUI_SimpleTiles": [ + [ + "TileMerge", + "TileSplit" + ], + { + "title_aux": "ComfyUI_SimpleTiles" + } + ], "https://github.com/kohya-ss/ControlNet-LLLite-ComfyUI": [ [ "LLLiteLoader" @@ -3188,7 +3208,9 @@ "NoxinChime", "NoxinPromptLoad", "NoxinPromptSave", - "NoxinScaledResolution" + "NoxinScaledResolution", + "NoxinSimpleMath", + "NoxinSplitPrompt" ], { "title_aux": "ComfyUI_NoxinNodes" @@ -3710,8 +3732,10 @@ [ "FreeU_LCM", "LCMGenerate", + "LCMGenerate_ReferenceOnly", "LCMGenerate_img2img", "LCMLoader", + "LCMLoader_ReferenceOnly", "LCMLoader_img2img", "LCM_outpaint_prep", "LoadImageNode_LCM", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 678beb61..ab5a4d91 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", @@ -884,106 +894,6 @@ ], "install_type": "git-clone", "description": "This is a copy of facerestore custom node 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.
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 twri/SDXL Prompt Styler." - }, - { - "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.

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.

" } ] } diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index e162a739..44cf0dc5 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -119,6 +119,7 @@ "BSZLatentbuster", "BSZPixelbuster", "BSZPixelbusterHelp", + "BSZPrincipledConditioning", "BSZPrincipledSampler", "BSZPrincipledScale" ], @@ -464,8 +465,11 @@ "BatchGLIGENSchedule", "BatchPromptSchedule", "BatchPromptScheduleEncodeSDXL", + "BatchPromptScheduleLatentInput", + "BatchPromptScheduleSDXLLatentInput", "BatchStringSchedule", "BatchValueSchedule", + "BatchValueScheduleLatentInput", "CosWave", "InvCosWave", "InvSinWave", @@ -1030,6 +1034,7 @@ "CR Composite Text", "CR Conditioning Input Switch", "CR ControlNet Input Switch", + "CR Draw Perspective Text", "CR Draw Text", "CR Float To Integer", "CR Float To String", @@ -1076,6 +1081,7 @@ "CR SDXL Style Text", "CR Seed", "CR Seed to Int", + "CR Simple Meme Template", "CR Split String", "CR Starburst Colors", "CR Starburst Lines", @@ -1988,7 +1994,7 @@ "Mikey Sampler Tiled", "Mikey Sampler Tiled Base Only", "MikeySamplerTiledAdvanced", - "OoobaPrompt", + "OobaPrompt", "PresetRatioSelector", "Prompt With SDXL", "Prompt With Style", @@ -2202,9 +2208,13 @@ ], "https://github.com/chibiace/ComfyUI-Chibi-Nodes": [ [ + "ConditionText", "ImageTool", + "LoadEmbedding", "Loader", - "Prompts" + "Prompts", + "SaveImages", + "Wildcards" ], { "title_aux": "ComfyUI-Chibi-Nodes" @@ -2709,12 +2719,22 @@ "CrossFadeImages", "GrowMaskWithBlur", "INTConstant", + "SomethingToString", "VRAM_Debug" ], { "title_aux": "KJNodes for ComfyUI" } ], + "https://github.com/kinfolk0117/ComfyUI_SimpleTiles": [ + [ + "TileMerge", + "TileSplit" + ], + { + "title_aux": "ComfyUI_SimpleTiles" + } + ], "https://github.com/kohya-ss/ControlNet-LLLite-ComfyUI": [ [ "LLLiteLoader" @@ -3188,7 +3208,9 @@ "NoxinChime", "NoxinPromptLoad", "NoxinPromptSave", - "NoxinScaledResolution" + "NoxinScaledResolution", + "NoxinSimpleMath", + "NoxinSplitPrompt" ], { "title_aux": "ComfyUI_NoxinNodes" @@ -3710,8 +3732,10 @@ [ "FreeU_LCM", "LCMGenerate", + "LCMGenerate_ReferenceOnly", "LCMGenerate_img2img", "LCMLoader", + "LCMLoader_ReferenceOnly", "LCMLoader_img2img", "LCM_outpaint_prep", "LoadImageNode_LCM", From d8bc0b2cc1f2fe5088185e61ecdc7762db022ec1 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 29 Oct 2023 11:26:42 +0900 Subject: [PATCH 24/59] update DB --- custom-node-list.json | 2 +- node_db/new/custom-node-list.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 4208d974..3ff49d41 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2303,7 +2303,7 @@ "https://github.com/0xbitches/ComfyUI-LCM" ], "install_type": "git-clone", - "description": "This custom node implements a Latent Consistency Model sampler in ComfyUI." + "description": "This custom node implements a Latent Consistency Model sampler in ComfyUI. (LCM)" }, { "author": "aszc-dev", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index ab5a4d91..3fb736a9 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -58,7 +58,7 @@ "https://github.com/0xbitches/ComfyUI-LCM" ], "install_type": "git-clone", - "description": "This custom node implements a Latent Consistency Model sampler in ComfyUI." + "description": "This custom node implements a Latent Consistency Model sampler in ComfyUI. (LCM)" }, { "author": "flowtyone", From ec066950be3868eda6e511505840a7755da673ed Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 29 Oct 2023 11:39:45 +0900 Subject: [PATCH 25/59] update DB --- extension-node-map.json | 1 + node_db/new/extension-node-map.json | 1 + 2 files changed, 2 insertions(+) diff --git a/extension-node-map.json b/extension-node-map.json index 44cf0dc5..b4aab7de 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -1526,6 +1526,7 @@ "Image Blending Mode", "Image Bloom Filter", "Image Bounds", + "Image Bounds to Console", "Image Canny Filter", "Image Chromatic Aberration", "Image Color Palette", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 44cf0dc5..b4aab7de 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -1526,6 +1526,7 @@ "Image Blending Mode", "Image Bloom Filter", "Image Bounds", + "Image Bounds to Console", "Image Canny Filter", "Image Chromatic Aberration", "Image Color Palette", From 8a7d024eca826af63140d23458701ab3a1c4ede8 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Mon, 30 Oct 2023 22:37:52 +0900 Subject: [PATCH 26/59] update DB --- custom-node-list.json | 20 ++++++++++++++++++ extension-node-map.json | 32 +++++++++++++++++++++++------ node_db/new/custom-node-list.json | 20 ++++++++++++++++++ node_db/new/extension-node-map.json | 32 +++++++++++++++++++++++------ 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 3ff49d41..90b638ec 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -1314,6 +1314,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", @@ -2365,6 +2375,16 @@ "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": "Ser-Hilary", "title": "SDXL_sizing", diff --git a/extension-node-map.json b/extension-node-map.json index b4aab7de..38c42ee6 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -117,6 +117,7 @@ "BSZLatentOffsetXL", "BSZLatentRGBAImage", "BSZLatentbuster", + "BSZMakeVPred", "BSZPixelbuster", "BSZPixelbusterHelp", "BSZPrincipledConditioning", @@ -449,6 +450,14 @@ "title_aux": "ComfyUI's ControlNet Auxiliary Preprocessors" } ], + "https://github.com/Fictiverse/ComfyUI_Fictiverse": [ + [ + "Color correction" + ], + { + "title_aux": "ComfyUI Fictiverse Nodes" + } + ], "https://github.com/FizzleDorf/ComfyUI-AIT": [ [ "AIT_Unet_Loader", @@ -739,6 +748,14 @@ "title_aux": "ComfyLiterals" } ], + "https://github.com/M1kep/ComfyUI-OtherVAEs": [ + [ + "OtherVAE_Taesd" + ], + { + "title_aux": "ComfyUI-OtherVAEs" + } + ], "https://github.com/M1kep/Comfy_KepKitchenSink": [ [ "KepRotateImage" @@ -1023,8 +1040,6 @@ "CR Apply Model Merge", "CR Apply Multi Upscale", "CR Apply Multi-ControlNet", - "CR Aspect Ratio", - "CR Aspect Ratio SDXL", "CR Batch Process Switch", "CR Checker Pattern", "CR Clip Input Switch", @@ -1039,7 +1054,6 @@ "CR Float To Integer", "CR Float To String", "CR Halftone Grid", - "CR Halftones", "CR Hires Fix Process Switch", "CR Image Input Switch", "CR Image Input Switch (4 way)", @@ -1071,13 +1085,11 @@ "CR Overlay Text", "CR Pipe Switch", "CR Polygons", - "CR Process Switch", "CR Prompt Text", "CR SD1.5 Aspect Ratio", "CR SDXL Aspect Ratio", "CR SDXL Base Prompt Encoder", "CR SDXL Prompt Mix Presets", - "CR SDXL Prompt Mixer", "CR SDXL Style Text", "CR Seed", "CR Seed to Int", @@ -1926,6 +1938,8 @@ ], "https://github.com/aszc-dev/ComfyUI-CoreMLSuite": [ [ + "CoreMLModelAdapter", + "CoreMLSampler", "CoreMLUNetLoader" ], { @@ -2020,7 +2034,8 @@ "TextPreserve", "Upscale Tile Calculator", "Wildcard Processor", - "WildcardAndLoraSyntaxProcessor" + "WildcardAndLoraSyntaxProcessor", + "WildcardOobaPrompt" ], { "title_aux": "Mikey Nodes" @@ -2729,6 +2744,7 @@ ], "https://github.com/kinfolk0117/ComfyUI_SimpleTiles": [ [ + "TileCalc", "TileMerge", "TileSplit" ], @@ -3732,10 +3748,13 @@ "https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy": [ [ "FreeU_LCM", + "ImageOutputToComfyNodes", + "ImageShuffle", "LCMGenerate", "LCMGenerate_ReferenceOnly", "LCMGenerate_img2img", "LCMLoader", + "LCMLoader_RefInpaint", "LCMLoader_ReferenceOnly", "LCMLoader_img2img", "LCM_outpaint_prep", @@ -4009,6 +4028,7 @@ "GroupChat", "Image_generation_Conditioning", "LM_Studio", + "LoadAPIconfig", "Memory_Excel", "Model_1", "Ollama", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 3fb736a9..7846ea77 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,25 @@ { "custom_nodes": [ + { + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index b4aab7de..38c42ee6 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -117,6 +117,7 @@ "BSZLatentOffsetXL", "BSZLatentRGBAImage", "BSZLatentbuster", + "BSZMakeVPred", "BSZPixelbuster", "BSZPixelbusterHelp", "BSZPrincipledConditioning", @@ -449,6 +450,14 @@ "title_aux": "ComfyUI's ControlNet Auxiliary Preprocessors" } ], + "https://github.com/Fictiverse/ComfyUI_Fictiverse": [ + [ + "Color correction" + ], + { + "title_aux": "ComfyUI Fictiverse Nodes" + } + ], "https://github.com/FizzleDorf/ComfyUI-AIT": [ [ "AIT_Unet_Loader", @@ -739,6 +748,14 @@ "title_aux": "ComfyLiterals" } ], + "https://github.com/M1kep/ComfyUI-OtherVAEs": [ + [ + "OtherVAE_Taesd" + ], + { + "title_aux": "ComfyUI-OtherVAEs" + } + ], "https://github.com/M1kep/Comfy_KepKitchenSink": [ [ "KepRotateImage" @@ -1023,8 +1040,6 @@ "CR Apply Model Merge", "CR Apply Multi Upscale", "CR Apply Multi-ControlNet", - "CR Aspect Ratio", - "CR Aspect Ratio SDXL", "CR Batch Process Switch", "CR Checker Pattern", "CR Clip Input Switch", @@ -1039,7 +1054,6 @@ "CR Float To Integer", "CR Float To String", "CR Halftone Grid", - "CR Halftones", "CR Hires Fix Process Switch", "CR Image Input Switch", "CR Image Input Switch (4 way)", @@ -1071,13 +1085,11 @@ "CR Overlay Text", "CR Pipe Switch", "CR Polygons", - "CR Process Switch", "CR Prompt Text", "CR SD1.5 Aspect Ratio", "CR SDXL Aspect Ratio", "CR SDXL Base Prompt Encoder", "CR SDXL Prompt Mix Presets", - "CR SDXL Prompt Mixer", "CR SDXL Style Text", "CR Seed", "CR Seed to Int", @@ -1926,6 +1938,8 @@ ], "https://github.com/aszc-dev/ComfyUI-CoreMLSuite": [ [ + "CoreMLModelAdapter", + "CoreMLSampler", "CoreMLUNetLoader" ], { @@ -2020,7 +2034,8 @@ "TextPreserve", "Upscale Tile Calculator", "Wildcard Processor", - "WildcardAndLoraSyntaxProcessor" + "WildcardAndLoraSyntaxProcessor", + "WildcardOobaPrompt" ], { "title_aux": "Mikey Nodes" @@ -2729,6 +2744,7 @@ ], "https://github.com/kinfolk0117/ComfyUI_SimpleTiles": [ [ + "TileCalc", "TileMerge", "TileSplit" ], @@ -3732,10 +3748,13 @@ "https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy": [ [ "FreeU_LCM", + "ImageOutputToComfyNodes", + "ImageShuffle", "LCMGenerate", "LCMGenerate_ReferenceOnly", "LCMGenerate_img2img", "LCMLoader", + "LCMLoader_RefInpaint", "LCMLoader_ReferenceOnly", "LCMLoader_img2img", "LCM_outpaint_prep", @@ -4009,6 +4028,7 @@ "GroupChat", "Image_generation_Conditioning", "LM_Studio", + "LoadAPIconfig", "Memory_Excel", "Model_1", "Ollama", From 0f583e6b790f30cc7c2b3a61d2d1c25d938c674d Mon Sep 17 00:00:00 2001 From: "dr.lt.data" Date: Tue, 31 Oct 2023 11:52:36 +0900 Subject: [PATCH 27/59] update DB --- custom-node-list.json | 10 +-- extension-node-map.json | 93 +++++++++++++++------------- node_db/legacy/custom-node-list.json | 10 +++ node_db/new/custom-node-list.json | 10 +++ node_db/new/extension-node-map.json | 93 +++++++++++++++------------- 5 files changed, 125 insertions(+), 91 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 90b638ec..01ce6c7f 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -151,14 +151,14 @@ "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 Install Models)" }, { - "author": "LucianoCirino", - "title": "Efficiency Nodes for ComfyUI", - "reference": "https://github.com/LucianoCirino/efficiency-nodes-comfyui", + "author": "jags111", + "title": "Efficiency Nodes for ComfyUI (jags version)", + "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.

NOTE: This node is originally created by LucianoCirino, but the original repository is no longer maintained and has been forked by a new maintainer. To use the forked version, you should uninstall the original version and REINSTALL this one.

" }, { "author": "Derfuu", diff --git a/extension-node-map.json b/extension-node-map.json index 38c42ee6..9e10bc09 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -636,6 +636,8 @@ "ADE_AnimateDiffLoRALoader", "ADE_AnimateDiffLoaderV1Advanced", "ADE_AnimateDiffLoaderWithContext", + "ADE_AnimateDiffModelSettings", + "ADE_AnimateDiffModelSettingsSimple", "ADE_AnimateDiffUniformContextOptions", "ADE_AnimateDiffUnload", "ADE_EmptyLatentImageLarge", @@ -693,49 +695,6 @@ "title_aux": "Canvas Tab" } ], - "https://github.com/LucianoCirino/efficiency-nodes-comfyui": [ - [ - "AnimateDiff Script", - "Apply ControlNet Stack", - "Control Net Stacker", - "Eff. Loader SDXL", - "Efficient Loader", - "HighRes-Fix Script", - "Image Overlay", - "Join XY Inputs of Same Type", - "KSampler (Efficient)", - "KSampler Adv. (Efficient)", - "KSampler SDXL (Eff.)", - "LoRA Stacker", - "Manual XY Entry Info", - "Noise Control Script", - "Pack SDXL Tuple", - "Tiled Upscaler Script", - "Unpack SDXL Tuple", - "XY Input: Add/Return Noise", - "XY Input: Aesthetic Score", - "XY Input: CFG Scale", - "XY Input: Checkpoint", - "XY Input: Clip Skip", - "XY Input: Control Net", - "XY Input: Control Net Plot", - "XY Input: Denoise", - "XY Input: LoRA", - "XY Input: LoRA Plot", - "XY Input: LoRA Stacks", - "XY Input: Manual XY Entry", - "XY Input: Prompt S/R", - "XY Input: Refiner On/Off", - "XY Input: Sampler/Scheduler", - "XY Input: Seeds++ Batch", - "XY Input: Steps", - "XY Input: VAE", - "XY Plot" - ], - { - "title_aux": "Efficiency Nodes for ComfyUI" - } - ], "https://github.com/M1kep/ComfyLiterals": [ [ "Checkpoint", @@ -1058,6 +1017,7 @@ "CR Image Input Switch", "CR Image Input Switch (4 way)", "CR Image Output", + "CR Image Panel", "CR Image Pipe Edit", "CR Image Pipe In", "CR Image Pipe Out", @@ -1086,6 +1046,7 @@ "CR Pipe Switch", "CR Polygons", "CR Prompt Text", + "CR Radial Gradient", "CR SD1.5 Aspect Ratio", "CR SDXL Aspect Ratio", "CR SDXL Base Prompt Encoder", @@ -1106,6 +1067,7 @@ "CR Text List To String", "CR Trigger", "CR Upscale Image", + "CR VAE Input Switch", "CR XY From Folder", "CR XY Grid", "CR XY Index", @@ -2708,6 +2670,49 @@ "title_aux": "FaceSwap" } ], + "https://github.com/jags111/efficiency-nodes-comfyui": [ + [ + "AnimateDiff Script", + "Apply ControlNet Stack", + "Control Net Stacker", + "Eff. Loader SDXL", + "Efficient Loader", + "HighRes-Fix Script", + "Image Overlay", + "Join XY Inputs of Same Type", + "KSampler (Efficient)", + "KSampler Adv. (Efficient)", + "KSampler SDXL (Eff.)", + "LoRA Stacker", + "Manual XY Entry Info", + "Noise Control Script", + "Pack SDXL Tuple", + "Tiled Upscaler Script", + "Unpack SDXL Tuple", + "XY Input: Add/Return Noise", + "XY Input: Aesthetic Score", + "XY Input: CFG Scale", + "XY Input: Checkpoint", + "XY Input: Clip Skip", + "XY Input: Control Net", + "XY Input: Control Net Plot", + "XY Input: Denoise", + "XY Input: LoRA", + "XY Input: LoRA Plot", + "XY Input: LoRA Stacks", + "XY Input: Manual XY Entry", + "XY Input: Prompt S/R", + "XY Input: Refiner On/Off", + "XY Input: Sampler/Scheduler", + "XY Input: Seeds++ Batch", + "XY Input: Steps", + "XY Input: VAE", + "XY Plot" + ], + { + "title_aux": "Efficiency Nodes for ComfyUI (jags version)" + } + ], "https://github.com/jjkramhoeft/ComfyUI-Jjk-Nodes": [ [ "JjkConcat", @@ -3898,6 +3903,7 @@ [ "Alternating KSampler (WLSH)", "Build Filename String (WLSH)", + "CLIP +/- w/Text Unified (WLSH)", "CLIP Positive-Negative (WLSH)", "CLIP Positive-Negative XL (WLSH)", "CLIP Positive-Negative XL w/Text (WLSH)", @@ -4029,6 +4035,7 @@ "Image_generation_Conditioning", "LM_Studio", "LoadAPIconfig", + "MemGPT", "Memory_Excel", "Model_1", "Ollama", diff --git a/node_db/legacy/custom-node-list.json b/node_db/legacy/custom-node-list.json index 211b6a9b..91869ad7 100644 --- a/node_db/legacy/custom-node-list.json +++ b/node_db/legacy/custom-node-list.json @@ -1,5 +1,15 @@ { "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.
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", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 7846ea77..04ace3a0 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "author": "jags111", + "title": "Efficiency Nodes for ComfyUI (jags version)", + "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.

NOTE: This node is originally created by LucianoCirino, but the original repository is no longer maintained and has been forked by a new maintainer. To use the forked version, you should uninstall the original version and REINSTALL this one.

" + }, { "author": "M1kep", "title": "ComfyUI-OtherVAEs", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 38c42ee6..9e10bc09 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -636,6 +636,8 @@ "ADE_AnimateDiffLoRALoader", "ADE_AnimateDiffLoaderV1Advanced", "ADE_AnimateDiffLoaderWithContext", + "ADE_AnimateDiffModelSettings", + "ADE_AnimateDiffModelSettingsSimple", "ADE_AnimateDiffUniformContextOptions", "ADE_AnimateDiffUnload", "ADE_EmptyLatentImageLarge", @@ -693,49 +695,6 @@ "title_aux": "Canvas Tab" } ], - "https://github.com/LucianoCirino/efficiency-nodes-comfyui": [ - [ - "AnimateDiff Script", - "Apply ControlNet Stack", - "Control Net Stacker", - "Eff. Loader SDXL", - "Efficient Loader", - "HighRes-Fix Script", - "Image Overlay", - "Join XY Inputs of Same Type", - "KSampler (Efficient)", - "KSampler Adv. (Efficient)", - "KSampler SDXL (Eff.)", - "LoRA Stacker", - "Manual XY Entry Info", - "Noise Control Script", - "Pack SDXL Tuple", - "Tiled Upscaler Script", - "Unpack SDXL Tuple", - "XY Input: Add/Return Noise", - "XY Input: Aesthetic Score", - "XY Input: CFG Scale", - "XY Input: Checkpoint", - "XY Input: Clip Skip", - "XY Input: Control Net", - "XY Input: Control Net Plot", - "XY Input: Denoise", - "XY Input: LoRA", - "XY Input: LoRA Plot", - "XY Input: LoRA Stacks", - "XY Input: Manual XY Entry", - "XY Input: Prompt S/R", - "XY Input: Refiner On/Off", - "XY Input: Sampler/Scheduler", - "XY Input: Seeds++ Batch", - "XY Input: Steps", - "XY Input: VAE", - "XY Plot" - ], - { - "title_aux": "Efficiency Nodes for ComfyUI" - } - ], "https://github.com/M1kep/ComfyLiterals": [ [ "Checkpoint", @@ -1058,6 +1017,7 @@ "CR Image Input Switch", "CR Image Input Switch (4 way)", "CR Image Output", + "CR Image Panel", "CR Image Pipe Edit", "CR Image Pipe In", "CR Image Pipe Out", @@ -1086,6 +1046,7 @@ "CR Pipe Switch", "CR Polygons", "CR Prompt Text", + "CR Radial Gradient", "CR SD1.5 Aspect Ratio", "CR SDXL Aspect Ratio", "CR SDXL Base Prompt Encoder", @@ -1106,6 +1067,7 @@ "CR Text List To String", "CR Trigger", "CR Upscale Image", + "CR VAE Input Switch", "CR XY From Folder", "CR XY Grid", "CR XY Index", @@ -2708,6 +2670,49 @@ "title_aux": "FaceSwap" } ], + "https://github.com/jags111/efficiency-nodes-comfyui": [ + [ + "AnimateDiff Script", + "Apply ControlNet Stack", + "Control Net Stacker", + "Eff. Loader SDXL", + "Efficient Loader", + "HighRes-Fix Script", + "Image Overlay", + "Join XY Inputs of Same Type", + "KSampler (Efficient)", + "KSampler Adv. (Efficient)", + "KSampler SDXL (Eff.)", + "LoRA Stacker", + "Manual XY Entry Info", + "Noise Control Script", + "Pack SDXL Tuple", + "Tiled Upscaler Script", + "Unpack SDXL Tuple", + "XY Input: Add/Return Noise", + "XY Input: Aesthetic Score", + "XY Input: CFG Scale", + "XY Input: Checkpoint", + "XY Input: Clip Skip", + "XY Input: Control Net", + "XY Input: Control Net Plot", + "XY Input: Denoise", + "XY Input: LoRA", + "XY Input: LoRA Plot", + "XY Input: LoRA Stacks", + "XY Input: Manual XY Entry", + "XY Input: Prompt S/R", + "XY Input: Refiner On/Off", + "XY Input: Sampler/Scheduler", + "XY Input: Seeds++ Batch", + "XY Input: Steps", + "XY Input: VAE", + "XY Plot" + ], + { + "title_aux": "Efficiency Nodes for ComfyUI (jags version)" + } + ], "https://github.com/jjkramhoeft/ComfyUI-Jjk-Nodes": [ [ "JjkConcat", @@ -3898,6 +3903,7 @@ [ "Alternating KSampler (WLSH)", "Build Filename String (WLSH)", + "CLIP +/- w/Text Unified (WLSH)", "CLIP Positive-Negative (WLSH)", "CLIP Positive-Negative XL (WLSH)", "CLIP Positive-Negative XL w/Text (WLSH)", @@ -4029,6 +4035,7 @@ "Image_generation_Conditioning", "LM_Studio", "LoadAPIconfig", + "MemGPT", "Memory_Excel", "Model_1", "Ollama", From 908a44429e8eb2c5a2e87d1d549b7e6dcdcfb2b6 Mon Sep 17 00:00:00 2001 From: "dr.lt.data" Date: Tue, 31 Oct 2023 12:43:22 +0900 Subject: [PATCH 28/59] update DB --- custom-node-list.json | 12 +++++- extension-node-map.json | 18 ++++++++- node_db/new/custom-node-list.json | 63 +++++------------------------ node_db/new/extension-node-map.json | 18 ++++++++- 4 files changed, 56 insertions(+), 55 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 01ce6c7f..a34fd730 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -152,7 +152,7 @@ }, { "author": "jags111", - "title": "Efficiency Nodes for ComfyUI (jags version)", + "title": "Efficiency Nodes for ComfyUI Version 2.0+", "reference": "https://github.com/jags111/efficiency-nodes-comfyui", "files": [ "https://github.com/jags111/efficiency-nodes-comfyui" @@ -2385,6 +2385,16 @@ "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 https://github.com/Extraltodeus/LoadLoraWithTags The aim of these custom nodes is to get an easy access to the tags used to trigger a lora." + }, { "author": "Ser-Hilary", "title": "SDXL_sizing", diff --git a/extension-node-map.json b/extension-node-map.json index 9e10bc09..16030cbd 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -2662,6 +2662,22 @@ "title_aux": "Eagle PNGInfo" } ], + "https://github.com/idrirap/ComfyUI-Lora-Auto-Trigger-Words": [ + [ + "FusionText", + "LoraLoaderAdvanced", + "LoraLoaderStackedAdvanced", + "LoraLoaderStackedVanilla", + "LoraLoaderVanilla", + "Randomizer", + "TagsFormater", + "TagsSelector", + "TextInputBasic" + ], + { + "title_aux": "ComfyUI-Lora-Auto-Trigger-Words" + } + ], "https://github.com/imb101/ComfyUI-FaceSwap": [ [ "FaceSwapNode" @@ -2710,7 +2726,7 @@ "XY Plot" ], { - "title_aux": "Efficiency Nodes for ComfyUI (jags version)" + "title_aux": "Efficiency Nodes for ComfyUI Version 2.0+" } ], "https://github.com/jjkramhoeft/ComfyUI-Jjk-Nodes": [ diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 04ace3a0..5552a547 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,8 +1,18 @@ { "custom_nodes": [ + { + "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 https://github.com/Extraltodeus/LoadLoraWithTags 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 (jags version)", + "title": "Efficiency Nodes for ComfyUI Version 2.0+", "reference": "https://github.com/jags111/efficiency-nodes-comfyui", "files": [ "https://github.com/jags111/efficiency-nodes-comfyui" @@ -873,57 +883,6 @@ ], "install_type": "git-clone", "description": "Nodes: KSampler (Advanced + Perp-Neg). Implementation of Perp-Neg
Includes Tonemap and CFG Rescale optionsComfyUI custom node to convert latent to RGB.

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 DiT(Scalable Diffusion Models with Transformers).

None of this code is stable, expect breaking changes if for some reason you want to use this.

" - }, - { - "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 facerestore custom node 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.
NOTE: To use this node, you need to download the face restoration model and face detection model from the 'Install models' menu." } ] } diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 9e10bc09..16030cbd 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -2662,6 +2662,22 @@ "title_aux": "Eagle PNGInfo" } ], + "https://github.com/idrirap/ComfyUI-Lora-Auto-Trigger-Words": [ + [ + "FusionText", + "LoraLoaderAdvanced", + "LoraLoaderStackedAdvanced", + "LoraLoaderStackedVanilla", + "LoraLoaderVanilla", + "Randomizer", + "TagsFormater", + "TagsSelector", + "TextInputBasic" + ], + { + "title_aux": "ComfyUI-Lora-Auto-Trigger-Words" + } + ], "https://github.com/imb101/ComfyUI-FaceSwap": [ [ "FaceSwapNode" @@ -2710,7 +2726,7 @@ "XY Plot" ], { - "title_aux": "Efficiency Nodes for ComfyUI (jags version)" + "title_aux": "Efficiency Nodes for ComfyUI Version 2.0+" } ], "https://github.com/jjkramhoeft/ComfyUI-Jjk-Nodes": [ From 466b8a5ef8527363dae9b7644dee7717e9448d42 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Wed, 1 Nov 2023 20:26:50 +0900 Subject: [PATCH 29/59] update DB --- custom-node-list.json | 13 +++++++++++-- extension-node-map.json | 28 ++++++++++++++++++++++------ node_db/dev/custom-node-list.json | 10 ++++++++++ node_db/new/custom-node-list.json | 13 +++++++++++-- node_db/new/extension-node-map.json | 28 ++++++++++++++++++++++------ 5 files changed, 76 insertions(+), 16 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index a34fd730..187d864e 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2100,7 +2100,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." }, @@ -2232,7 +2231,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", @@ -2395,6 +2394,16 @@ "install_type": "git-clone", "description": "This project is a fork of https://github.com/Extraltodeus/LoadLoraWithTags 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": "Ser-Hilary", "title": "SDXL_sizing", diff --git a/extension-node-map.json b/extension-node-map.json index 16030cbd..bda7abb5 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -452,7 +452,8 @@ ], "https://github.com/Fictiverse/ComfyUI_Fictiverse": [ [ - "Color correction" + "Color correction", + "Displace Images with Mask" ], { "title_aux": "ComfyUI Fictiverse Nodes" @@ -637,6 +638,7 @@ "ADE_AnimateDiffLoaderV1Advanced", "ADE_AnimateDiffLoaderWithContext", "ADE_AnimateDiffModelSettings", + "ADE_AnimateDiffModelSettingsAdvancedAttnStrengths", "ADE_AnimateDiffModelSettingsSimple", "ADE_AnimateDiffUniformContextOptions", "ADE_AnimateDiffUnload", @@ -994,6 +996,8 @@ ], "https://github.com/RockOfFire/ComfyUI_Comfyroll_CustomNodes": [ [ + "CR Add Annotation", + "CR Apply Annotations", "CR Apply ControlNet", "CR Apply LoRA Stack", "CR Apply Model Merge", @@ -1033,7 +1037,6 @@ "CR Latent Input Switch", "CR LoRA Stack", "CR Load LoRA", - "CR Load XY Annotation From File", "CR Mask Text", "CR Model Input Switch", "CR Model Merge Stack", @@ -1042,9 +1045,12 @@ "CR Module Pipe Loader", "CR Multi Upscale Stack", "CR Multi-ControlNet Stack", + "CR Multi-Panel Meme Template", "CR Overlay Text", + "CR Page Layout", "CR Pipe Switch", "CR Polygons", + "CR Popular Meme Templates", "CR Prompt Text", "CR Radial Gradient", "CR SD1.5 Aspect Ratio", @@ -1054,6 +1060,7 @@ "CR SDXL Style Text", "CR Seed", "CR Seed to Int", + "CR Simple Annotations", "CR Simple Meme Template", "CR Split String", "CR Starburst Colors", @@ -1069,12 +1076,10 @@ "CR Upscale Image", "CR VAE Input Switch", "CR XY From Folder", - "CR XY Grid", "CR XY Index", "CR XY Interpolate", "CR XY List", - "CR XY Save Grid Image", - "CR XYZ Index" + "CR XY Save Grid Image" ], { "title_aux": "ComfyUI_Comfyroll_CustomNodes" @@ -1790,6 +1795,14 @@ "title_aux": "DynamicPrompts Custom Nodes" } ], + "https://github.com/aianimation55/ComfyUI-FatLabels": [ + [ + "FatLabels" + ], + { + "title_aux": "Comfy UI FatLabels" + } + ], "https://github.com/alpertunga-bile/prompt-generator-comfyui": [ [ "Prompt Generator" @@ -2742,6 +2755,7 @@ ], "https://github.com/kijai/ComfyUI-KJNodes": [ [ + "ColorMatch", "ColorToMask", "ConditioningMultiCombine", "ConditioningSetMaskAndCombine", @@ -2754,6 +2768,7 @@ "CreateGradientMask", "CreateTextMask", "CrossFadeImages", + "EmptyLatentImagePresets", "GrowMaskWithBlur", "INTConstant", "SomethingToString", @@ -2917,6 +2932,7 @@ "ImpactImageInfo", "ImpactInt", "ImpactInversedSwitch", + "ImpactIsNotEmptySEGS", "ImpactKSamplerAdvancedBasicPipe", "ImpactKSamplerBasicPipe", "ImpactLogger", @@ -3053,6 +3069,7 @@ "RetrieveBackendData //Inspire", "RetrieveBackendDataNumberKey //Inspire", "ShowCachedInfo //Inspire", + "TilePreprocessor_Provider_for_SEGS //Inspire", "UnzipPrompt //Inspire", "WildcardEncode //Inspire", "XY Input: Lora Block Weight //Inspire", @@ -3532,7 +3549,6 @@ "ColorCorrect", "DeepDanbooruCaption", "DependenciesEdit", - "FaceAnalyze", "Fooocus_KSampler", "Fooocus_KSamplerAdvanced", "GetBoolFromJson", diff --git a/node_db/dev/custom-node-list.json b/node_db/dev/custom-node-list.json index b12fbd3d..03ace4d5 100644 --- a/node_db/dev/custom-node-list.json +++ b/node_db/dev/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_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", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 5552a547..7ae95433 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", @@ -239,7 +249,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", @@ -458,7 +468,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." }, diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 16030cbd..bda7abb5 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -452,7 +452,8 @@ ], "https://github.com/Fictiverse/ComfyUI_Fictiverse": [ [ - "Color correction" + "Color correction", + "Displace Images with Mask" ], { "title_aux": "ComfyUI Fictiverse Nodes" @@ -637,6 +638,7 @@ "ADE_AnimateDiffLoaderV1Advanced", "ADE_AnimateDiffLoaderWithContext", "ADE_AnimateDiffModelSettings", + "ADE_AnimateDiffModelSettingsAdvancedAttnStrengths", "ADE_AnimateDiffModelSettingsSimple", "ADE_AnimateDiffUniformContextOptions", "ADE_AnimateDiffUnload", @@ -994,6 +996,8 @@ ], "https://github.com/RockOfFire/ComfyUI_Comfyroll_CustomNodes": [ [ + "CR Add Annotation", + "CR Apply Annotations", "CR Apply ControlNet", "CR Apply LoRA Stack", "CR Apply Model Merge", @@ -1033,7 +1037,6 @@ "CR Latent Input Switch", "CR LoRA Stack", "CR Load LoRA", - "CR Load XY Annotation From File", "CR Mask Text", "CR Model Input Switch", "CR Model Merge Stack", @@ -1042,9 +1045,12 @@ "CR Module Pipe Loader", "CR Multi Upscale Stack", "CR Multi-ControlNet Stack", + "CR Multi-Panel Meme Template", "CR Overlay Text", + "CR Page Layout", "CR Pipe Switch", "CR Polygons", + "CR Popular Meme Templates", "CR Prompt Text", "CR Radial Gradient", "CR SD1.5 Aspect Ratio", @@ -1054,6 +1060,7 @@ "CR SDXL Style Text", "CR Seed", "CR Seed to Int", + "CR Simple Annotations", "CR Simple Meme Template", "CR Split String", "CR Starburst Colors", @@ -1069,12 +1076,10 @@ "CR Upscale Image", "CR VAE Input Switch", "CR XY From Folder", - "CR XY Grid", "CR XY Index", "CR XY Interpolate", "CR XY List", - "CR XY Save Grid Image", - "CR XYZ Index" + "CR XY Save Grid Image" ], { "title_aux": "ComfyUI_Comfyroll_CustomNodes" @@ -1790,6 +1795,14 @@ "title_aux": "DynamicPrompts Custom Nodes" } ], + "https://github.com/aianimation55/ComfyUI-FatLabels": [ + [ + "FatLabels" + ], + { + "title_aux": "Comfy UI FatLabels" + } + ], "https://github.com/alpertunga-bile/prompt-generator-comfyui": [ [ "Prompt Generator" @@ -2742,6 +2755,7 @@ ], "https://github.com/kijai/ComfyUI-KJNodes": [ [ + "ColorMatch", "ColorToMask", "ConditioningMultiCombine", "ConditioningSetMaskAndCombine", @@ -2754,6 +2768,7 @@ "CreateGradientMask", "CreateTextMask", "CrossFadeImages", + "EmptyLatentImagePresets", "GrowMaskWithBlur", "INTConstant", "SomethingToString", @@ -2917,6 +2932,7 @@ "ImpactImageInfo", "ImpactInt", "ImpactInversedSwitch", + "ImpactIsNotEmptySEGS", "ImpactKSamplerAdvancedBasicPipe", "ImpactKSamplerBasicPipe", "ImpactLogger", @@ -3053,6 +3069,7 @@ "RetrieveBackendData //Inspire", "RetrieveBackendDataNumberKey //Inspire", "ShowCachedInfo //Inspire", + "TilePreprocessor_Provider_for_SEGS //Inspire", "UnzipPrompt //Inspire", "WildcardEncode //Inspire", "XY Input: Lora Block Weight //Inspire", @@ -3532,7 +3549,6 @@ "ColorCorrect", "DeepDanbooruCaption", "DependenciesEdit", - "FaceAnalyze", "Fooocus_KSampler", "Fooocus_KSamplerAdvanced", "GetBoolFromJson", From 9f8b37b94e949943d52db8d22119ea9ab1621b46 Mon Sep 17 00:00:00 2001 From: noEmbryo Date: Wed, 1 Nov 2023 17:16:01 +0200 Subject: [PATCH 30/59] Update custom-node-list.json New nodes from noEmbryo --- custom-node-list.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/custom-node-list.json b/custom-node-list.json index 187d864e..3df5901a 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2675,6 +2675,16 @@ ], "install_type": "unzip", "description": "This is a node to convert an image into a CMYK Halftone dot image." + }, + { + "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." } ] } From ea0c38b846fff71383f572563196fd446994716c Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Thu, 2 Nov 2023 18:26:08 +0900 Subject: [PATCH 31/59] fix: fallback when ssl error on model downloading --- __init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 2174dafd..15ad8fba 100644 --- a/__init__.py +++ b/__init__.py @@ -56,7 +56,7 @@ sys.path.append('../..') from torchvision.datasets.utils import download_url # ensure .js -print("### Loading: ComfyUI-Manager (V0.36)") +print("### Loading: ComfyUI-Manager (V0.36.1)") comfy_ui_required_revision = 1240 comfy_ui_revision = "Unknown" @@ -1132,7 +1132,13 @@ 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) + + try: + download_url(json_data['url'], model_dir) + except: + fallback_url = json_data['url'].replace('https', 'http') + download_url(fallback_url, model_dir) + return web.json_response({}, content_type='application/json') else: res = download_url_with_agent(json_data['url'], model_path) From 3fa127f47f44bca900db7d00a0f948d8e2482e7c Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Thu, 2 Nov 2023 23:51:39 +0900 Subject: [PATCH 32/59] update DB --- custom-node-list.json | 40 +++++++++++++++++++++ extension-node-map.json | 56 ++++++++++++++++++++++++++--- node_db/new/custom-node-list.json | 40 +++++++++++++++++++++ node_db/new/extension-node-map.json | 56 ++++++++++++++++++++++++++--- 4 files changed, 182 insertions(+), 10 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 187d864e..c70a7896 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2253,6 +2253,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", @@ -2404,6 +2414,36 @@ "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", + "reference": "https://github.com/noembryo/ComfyUI-noEmbryo", + "files": [ + "https://github.com/noembryo/ComfyUI-noEmbryo" + ], + "install_type": "git-clone", + "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", diff --git a/extension-node-map.json b/extension-node-map.json index bda7abb5..6e57915c 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -924,6 +924,17 @@ "title_aux": "QRNG_Node_ComfyUI" } ], + "https://github.com/ParmanBabra/ComfyUI-Malefish-Custom-Scripts": [ + [ + "CSVPromptsLoader", + "CombinePrompt", + "MultiLoraLoader", + "RandomPrompt" + ], + { + "title_aux": "ComfyUI-Malefish-Custom-Scripts" + } + ], "https://github.com/Pfaeff/pfaeff-comfyui": [ [ "AstropulsePixelDetector", @@ -996,8 +1007,6 @@ ], "https://github.com/RockOfFire/ComfyUI_Comfyroll_CustomNodes": [ [ - "CR Add Annotation", - "CR Apply Annotations", "CR Apply ControlNet", "CR Apply LoRA Stack", "CR Apply Model Merge", @@ -1008,6 +1017,7 @@ "CR Clip Input Switch", "CR Color Bars", "CR Color Gradient", + "CR Color Panel", "CR Color Tint", "CR Composite Text", "CR Conditioning Input Switch", @@ -1018,6 +1028,8 @@ "CR Float To String", "CR Halftone Grid", "CR Hires Fix Process Switch", + "CR Image Border", + "CR Image Grid Panel", "CR Image Input Switch", "CR Image Input Switch (4 way)", "CR Image Output", @@ -1026,6 +1038,7 @@ "CR Image Pipe In", "CR Image Pipe Out", "CR Image Size", + "CR Image XY Panel", "CR Img2Img Process Switch", "CR Index", "CR Index Increment", @@ -1045,12 +1058,10 @@ "CR Module Pipe Loader", "CR Multi Upscale Stack", "CR Multi-ControlNet Stack", - "CR Multi-Panel Meme Template", "CR Overlay Text", "CR Page Layout", "CR Pipe Switch", "CR Polygons", - "CR Popular Meme Templates", "CR Prompt Text", "CR Radial Gradient", "CR SD1.5 Aspect Ratio", @@ -1060,8 +1071,8 @@ "CR SDXL Style Text", "CR Seed", "CR Seed to Int", - "CR Simple Annotations", "CR Simple Meme Template", + "CR Simple Text Panel", "CR Split String", "CR Starburst Colors", "CR Starburst Lines", @@ -1075,6 +1086,7 @@ "CR Trigger", "CR Upscale Image", "CR VAE Input Switch", + "CR Value", "CR XY From Folder", "CR XY Index", "CR XY Interpolate", @@ -1274,6 +1286,14 @@ "title_aux": "TGu Utilities" } ], + "https://github.com/THtianhao/ComfyUI-FaceChain": [ + [ + "FC_LoraMerge" + ], + { + "title_aux": "ComfyUI-FaceChain" + } + ], "https://github.com/THtianhao/ComfyUI-Portrait-Maker": [ [ "PM_BoxCropImage", @@ -2769,6 +2789,7 @@ "CreateTextMask", "CrossFadeImages", "EmptyLatentImagePresets", + "FloatConstant", "GrowMaskWithBlur", "INTConstant", "SomethingToString", @@ -3225,6 +3246,14 @@ "title_aux": "ComfyUI - Text Overlay Plugin" } ], + "https://github.com/mikkel/comfyui-mask-boundingbox": [ + [ + "Mask Bounding Box" + ], + { + "title_aux": "ComfyUI - Mask Bounding Box" + } + ], "https://github.com/mlinmg/ComfyUI-LaMA-Preprocessor": [ [ "LaMaPreprocessor", @@ -3258,6 +3287,23 @@ "title_aux": "comfyUI_Nodes_nicolai256" } ], + "https://github.com/noembryo/ComfyUI-noEmbryo": [ + [ + "PromptTermList1", + "PromptTermList2", + "PromptTermList3", + "PromptTermList4", + "PromptTermList5", + "PromptTermList6" + ], + { + "author": "noEmbryo", + "description": "Some useful nodes.", + "nickname": "noEmbryo", + "title": "X Node", + "title_aux": "ComfyUI-noEmbryo" + } + ], "https://github.com/noxinias/ComfyUI_NoxinNodes": [ [ "NoxinChime", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 7ae95433..e36b1499 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,45 @@ { "custom_nodes": [ + { + "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", + "reference": "https://github.com/noembryo/ComfyUI-noEmbryo", + "files": [ + "https://github.com/noembryo/ComfyUI-noEmbryo" + ], + "install_type": "git-clone", + "description": "PromptTermList (1-6) are some nodes that help with the creation of Prompts inside ComfyUI." + }, { "author": "aianimation55", "title": "Comfy UI FatLabels", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index bda7abb5..6e57915c 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -924,6 +924,17 @@ "title_aux": "QRNG_Node_ComfyUI" } ], + "https://github.com/ParmanBabra/ComfyUI-Malefish-Custom-Scripts": [ + [ + "CSVPromptsLoader", + "CombinePrompt", + "MultiLoraLoader", + "RandomPrompt" + ], + { + "title_aux": "ComfyUI-Malefish-Custom-Scripts" + } + ], "https://github.com/Pfaeff/pfaeff-comfyui": [ [ "AstropulsePixelDetector", @@ -996,8 +1007,6 @@ ], "https://github.com/RockOfFire/ComfyUI_Comfyroll_CustomNodes": [ [ - "CR Add Annotation", - "CR Apply Annotations", "CR Apply ControlNet", "CR Apply LoRA Stack", "CR Apply Model Merge", @@ -1008,6 +1017,7 @@ "CR Clip Input Switch", "CR Color Bars", "CR Color Gradient", + "CR Color Panel", "CR Color Tint", "CR Composite Text", "CR Conditioning Input Switch", @@ -1018,6 +1028,8 @@ "CR Float To String", "CR Halftone Grid", "CR Hires Fix Process Switch", + "CR Image Border", + "CR Image Grid Panel", "CR Image Input Switch", "CR Image Input Switch (4 way)", "CR Image Output", @@ -1026,6 +1038,7 @@ "CR Image Pipe In", "CR Image Pipe Out", "CR Image Size", + "CR Image XY Panel", "CR Img2Img Process Switch", "CR Index", "CR Index Increment", @@ -1045,12 +1058,10 @@ "CR Module Pipe Loader", "CR Multi Upscale Stack", "CR Multi-ControlNet Stack", - "CR Multi-Panel Meme Template", "CR Overlay Text", "CR Page Layout", "CR Pipe Switch", "CR Polygons", - "CR Popular Meme Templates", "CR Prompt Text", "CR Radial Gradient", "CR SD1.5 Aspect Ratio", @@ -1060,8 +1071,8 @@ "CR SDXL Style Text", "CR Seed", "CR Seed to Int", - "CR Simple Annotations", "CR Simple Meme Template", + "CR Simple Text Panel", "CR Split String", "CR Starburst Colors", "CR Starburst Lines", @@ -1075,6 +1086,7 @@ "CR Trigger", "CR Upscale Image", "CR VAE Input Switch", + "CR Value", "CR XY From Folder", "CR XY Index", "CR XY Interpolate", @@ -1274,6 +1286,14 @@ "title_aux": "TGu Utilities" } ], + "https://github.com/THtianhao/ComfyUI-FaceChain": [ + [ + "FC_LoraMerge" + ], + { + "title_aux": "ComfyUI-FaceChain" + } + ], "https://github.com/THtianhao/ComfyUI-Portrait-Maker": [ [ "PM_BoxCropImage", @@ -2769,6 +2789,7 @@ "CreateTextMask", "CrossFadeImages", "EmptyLatentImagePresets", + "FloatConstant", "GrowMaskWithBlur", "INTConstant", "SomethingToString", @@ -3225,6 +3246,14 @@ "title_aux": "ComfyUI - Text Overlay Plugin" } ], + "https://github.com/mikkel/comfyui-mask-boundingbox": [ + [ + "Mask Bounding Box" + ], + { + "title_aux": "ComfyUI - Mask Bounding Box" + } + ], "https://github.com/mlinmg/ComfyUI-LaMA-Preprocessor": [ [ "LaMaPreprocessor", @@ -3258,6 +3287,23 @@ "title_aux": "comfyUI_Nodes_nicolai256" } ], + "https://github.com/noembryo/ComfyUI-noEmbryo": [ + [ + "PromptTermList1", + "PromptTermList2", + "PromptTermList3", + "PromptTermList4", + "PromptTermList5", + "PromptTermList6" + ], + { + "author": "noEmbryo", + "description": "Some useful nodes.", + "nickname": "noEmbryo", + "title": "X Node", + "title_aux": "ComfyUI-noEmbryo" + } + ], "https://github.com/noxinias/ComfyUI_NoxinNodes": [ [ "NoxinChime", From 21d70875e5ffab0155100ed12c4c1371bf2130af Mon Sep 17 00:00:00 2001 From: Matan Date: Thu, 2 Nov 2023 19:20:41 +0200 Subject: [PATCH 33/59] Update custom-node-list.json with comfyui-serving-manager --- custom-node-list.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/custom-node-list.json b/custom-node-list.json index c70a7896..fd821461 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2652,6 +2652,16 @@ "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": "Smuzzies", "title": "Chatbox Overlay node for ComfyUI", From 7e498020ee0ad5390763c2bec709f0e89df164a4 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Fri, 3 Nov 2023 19:34:27 +0900 Subject: [PATCH 34/59] update DB --- custom-node-list.json | 20 +++-------- extension-node-map.json | 54 +++++++++++++++++++---------- node_db/new/custom-node-list.json | 20 ++++++++--- node_db/new/extension-node-map.json | 54 +++++++++++++++++++---------- 4 files changed, 92 insertions(+), 56 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index bac08ca9..6445ad79 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2415,14 +2415,14 @@ "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", + "author": "noEmbryo", + "title": "ComfyUI-noEmbryo nodes", "reference": "https://github.com/noembryo/ComfyUI-noEmbryo", "files": [ - "https://github.com/noembryo/ComfyUI-noEmbryo" + "https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/nodes.py" ], - "install_type": "git-clone", - "description": "PromptTermList (1-6) are some nodes that help with the creation of Prompts inside ComfyUI." + "install_type": "copy", + "description": "PromptTermList (1-6): are some nodes that help with the creation of Prompts inside ComfyUI." }, { "author": "mikkel", @@ -2725,16 +2725,6 @@ ], "install_type": "unzip", "description": "This is a node to convert an image into a CMYK Halftone dot image." - }, - { - "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." } ] } diff --git a/extension-node-map.json b/extension-node-map.json index 6e57915c..1c0e5073 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -413,6 +413,7 @@ "https://github.com/Fannovel16/comfyui_controlnet_aux": [ [ "AIO_Preprocessor", + "AnimeFace_SemSegPreprocessor", "AnimeLineArtPreprocessor", "BAE-NormalMapPreprocessor", "BinaryPreprocessor", @@ -501,13 +502,18 @@ ], "https://github.com/GMapeSplat/ComfyUI_ezXY": [ [ - "ItemFromList", + "ConcatenateString", "IterationDriver", + "JoinImages", "LineToConsole", + "NumberFromList", "NumbersToList", "PlotImages", + "StringFromList", + "StringToLabel", "StringToList", "ezMath", + "ezXY_AssemblePlot", "ezXY_Driver" ], { @@ -1933,6 +1939,8 @@ ], "https://github.com/aszc-dev/ComfyUI-CoreMLSuite": [ [ + "Core ML LCM Converter", + "Core ML LCM Sampler", "CoreMLModelAdapter", "CoreMLSampler", "CoreMLUNetLoader" @@ -3133,6 +3141,17 @@ "title_aux": "Recommended Resolution Calculator" } ], + "https://github.com/matan1905/ComfyUI-Serving-Toolkit": [ + [ + "DiscordServing", + "ServingInputNumber", + "ServingInputText", + "ServingOutput" + ], + { + "title_aux": "ComfyUI Serving toolkit" + } + ], "https://github.com/mav-rik/facerestore_cf": [ [ "CropFace", @@ -3287,23 +3306,6 @@ "title_aux": "comfyUI_Nodes_nicolai256" } ], - "https://github.com/noembryo/ComfyUI-noEmbryo": [ - [ - "PromptTermList1", - "PromptTermList2", - "PromptTermList3", - "PromptTermList4", - "PromptTermList5", - "PromptTermList6" - ], - { - "author": "noEmbryo", - "description": "Some useful nodes.", - "nickname": "noEmbryo", - "title": "X Node", - "title_aux": "ComfyUI-noEmbryo" - } - ], "https://github.com/noxinias/ComfyUI_NoxinNodes": [ [ "NoxinChime", @@ -3836,10 +3838,13 @@ "LCMGenerate", "LCMGenerate_ReferenceOnly", "LCMGenerate_img2img", + "LCMGenerate_img2img_controlnet", "LCMLoader", "LCMLoader_RefInpaint", "LCMLoader_ReferenceOnly", + "LCMLoader_controlnet", "LCMLoader_img2img", + "LCMT2IAdapter", "LCM_outpaint_prep", "LoadImageNode_LCM", "SaveImage_LCM" @@ -4235,6 +4240,19 @@ "title_aux": "Cute Comfy" } ], + "https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/nodes.py": [ + [ + "PromptTermList1", + "PromptTermList2", + "PromptTermList3", + "PromptTermList4", + "PromptTermList5", + "PromptTermList6" + ], + { + "title_aux": "ComfyUI-noEmbryo nodes" + } + ], "https://raw.githubusercontent.com/throttlekitty/SDXLCustomAspectRatio/main/SDXLAspectRatio.py": [ [ "SDXLAspectRatio" diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index e36b1499..fb0c89f9 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", @@ -31,14 +41,14 @@ "description": "Nodes:FC_LoraMerge." }, { - "author": "noembryo", - "title": "ComfyUI-noEmbryo", + "author": "noEmbryo", + "title": "ComfyUI-noEmbryo nodes", "reference": "https://github.com/noembryo/ComfyUI-noEmbryo", "files": [ - "https://github.com/noembryo/ComfyUI-noEmbryo" + "https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/nodes.py" ], - "install_type": "git-clone", - "description": "PromptTermList (1-6) are some nodes that help with the creation of Prompts inside ComfyUI." + "install_type": "copy", + "description": "PromptTermList (1-6): are some nodes that help with the creation of Prompts inside ComfyUI." }, { "author": "aianimation55", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 6e57915c..1c0e5073 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -413,6 +413,7 @@ "https://github.com/Fannovel16/comfyui_controlnet_aux": [ [ "AIO_Preprocessor", + "AnimeFace_SemSegPreprocessor", "AnimeLineArtPreprocessor", "BAE-NormalMapPreprocessor", "BinaryPreprocessor", @@ -501,13 +502,18 @@ ], "https://github.com/GMapeSplat/ComfyUI_ezXY": [ [ - "ItemFromList", + "ConcatenateString", "IterationDriver", + "JoinImages", "LineToConsole", + "NumberFromList", "NumbersToList", "PlotImages", + "StringFromList", + "StringToLabel", "StringToList", "ezMath", + "ezXY_AssemblePlot", "ezXY_Driver" ], { @@ -1933,6 +1939,8 @@ ], "https://github.com/aszc-dev/ComfyUI-CoreMLSuite": [ [ + "Core ML LCM Converter", + "Core ML LCM Sampler", "CoreMLModelAdapter", "CoreMLSampler", "CoreMLUNetLoader" @@ -3133,6 +3141,17 @@ "title_aux": "Recommended Resolution Calculator" } ], + "https://github.com/matan1905/ComfyUI-Serving-Toolkit": [ + [ + "DiscordServing", + "ServingInputNumber", + "ServingInputText", + "ServingOutput" + ], + { + "title_aux": "ComfyUI Serving toolkit" + } + ], "https://github.com/mav-rik/facerestore_cf": [ [ "CropFace", @@ -3287,23 +3306,6 @@ "title_aux": "comfyUI_Nodes_nicolai256" } ], - "https://github.com/noembryo/ComfyUI-noEmbryo": [ - [ - "PromptTermList1", - "PromptTermList2", - "PromptTermList3", - "PromptTermList4", - "PromptTermList5", - "PromptTermList6" - ], - { - "author": "noEmbryo", - "description": "Some useful nodes.", - "nickname": "noEmbryo", - "title": "X Node", - "title_aux": "ComfyUI-noEmbryo" - } - ], "https://github.com/noxinias/ComfyUI_NoxinNodes": [ [ "NoxinChime", @@ -3836,10 +3838,13 @@ "LCMGenerate", "LCMGenerate_ReferenceOnly", "LCMGenerate_img2img", + "LCMGenerate_img2img_controlnet", "LCMLoader", "LCMLoader_RefInpaint", "LCMLoader_ReferenceOnly", + "LCMLoader_controlnet", "LCMLoader_img2img", + "LCMT2IAdapter", "LCM_outpaint_prep", "LoadImageNode_LCM", "SaveImage_LCM" @@ -4235,6 +4240,19 @@ "title_aux": "Cute Comfy" } ], + "https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/nodes.py": [ + [ + "PromptTermList1", + "PromptTermList2", + "PromptTermList3", + "PromptTermList4", + "PromptTermList5", + "PromptTermList6" + ], + { + "title_aux": "ComfyUI-noEmbryo nodes" + } + ], "https://raw.githubusercontent.com/throttlekitty/SDXLCustomAspectRatio/main/SDXLAspectRatio.py": [ [ "SDXLAspectRatio" From e345f641d2a3a50251bb11fabd546b2f2c2b399c Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Fri, 3 Nov 2023 20:31:20 +0900 Subject: [PATCH 35/59] update DB --- custom-node-list.json | 2 +- extension-node-map.json | 2 +- node_db/new/custom-node-list.json | 90 ----------------------------- node_db/new/extension-node-map.json | 2 +- 4 files changed, 3 insertions(+), 93 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 6445ad79..10493209 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -1176,7 +1176,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" diff --git a/extension-node-map.json b/extension-node-map.json index 1c0e5073..72104dd3 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -525,7 +525,7 @@ "ReActorFaceSwap" ], { - "title_aux": "ReActor Node 0.1.0 for ComfyUI" + "title_aux": "ReActor Node for ComfyUI" } ], "https://github.com/JPS-GER/ComfyUI_JPS-Nodes": [ diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index fb0c89f9..c7ce7fe0 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -852,96 +852,6 @@ ], "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.

NOTE: This node replaces the execution of ComfyUI for iterative processing functionality.

" - }, - { - "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 AnimateDiff, created by ArtVentureX.

Improved AnimateDiff integration for ComfyUI, adapts from sd-webui-animatediff.

Download one or more motion models from Original Models | Finetuned Models. See README for additional model links and usage. Put the model weights under ComfyUI/custom_nodes/ComfyUI-AnimateDiff-Evolved/models. You are free to rename the models, but keeping original names will ease use when sharing your workflow.

" - }, - { - "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 Perp-Neg
Includes Tonemap and CFG Rescale optionsComfyUI custom node to convert latent to RGB.

WARNING: Experimental code, might have incompatibilities and edge cases." } ] } diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 1c0e5073..72104dd3 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -525,7 +525,7 @@ "ReActorFaceSwap" ], { - "title_aux": "ReActor Node 0.1.0 for ComfyUI" + "title_aux": "ReActor Node for ComfyUI" } ], "https://github.com/JPS-GER/ComfyUI_JPS-Nodes": [ From 2d6633ec6cd22993f105875bdb8562fda15e8073 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sat, 4 Nov 2023 12:15:32 +0900 Subject: [PATCH 36/59] update DB --- extension-node-map.json | 3 +++ node_db/dev/custom-node-list.json | 10 ++++++++++ node_db/new/extension-node-map.json | 3 +++ 3 files changed, 16 insertions(+) diff --git a/extension-node-map.json b/extension-node-map.json index 72104dd3..b089fa19 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -1018,6 +1018,7 @@ "CR Apply Model Merge", "CR Apply Multi Upscale", "CR Apply Multi-ControlNet", + "CR Aspect Ratio SDXL", "CR Batch Process Switch", "CR Checker Pattern", "CR Clip Input Switch", @@ -2458,6 +2459,7 @@ ], "https://github.com/dimtoneff/ComfyUI-PixelArt-Detector": [ [ + "PixelArtAddDitherPattern", "PixelArtDetectorConverter", "PixelArtDetectorSave", "PixelArtDetectorToImage", @@ -2798,6 +2800,7 @@ "CrossFadeImages", "EmptyLatentImagePresets", "FloatConstant", + "GetImageRangeFromBatch", "GrowMaskWithBlur", "INTConstant", "SomethingToString", diff --git a/node_db/dev/custom-node-list.json b/node_db/dev/custom-node-list.json index 03ace4d5..8282ed83 100644 --- a/node_db/dev/custom-node-list.json +++ b/node_db/dev/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 72104dd3..b089fa19 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -1018,6 +1018,7 @@ "CR Apply Model Merge", "CR Apply Multi Upscale", "CR Apply Multi-ControlNet", + "CR Aspect Ratio SDXL", "CR Batch Process Switch", "CR Checker Pattern", "CR Clip Input Switch", @@ -2458,6 +2459,7 @@ ], "https://github.com/dimtoneff/ComfyUI-PixelArt-Detector": [ [ + "PixelArtAddDitherPattern", "PixelArtDetectorConverter", "PixelArtDetectorSave", "PixelArtDetectorToImage", @@ -2798,6 +2800,7 @@ "CrossFadeImages", "EmptyLatentImagePresets", "FloatConstant", + "GetImageRangeFromBatch", "GrowMaskWithBlur", "INTConstant", "SomethingToString", From 7fbe34f8dbf95e913b2854c5d28f5605cda0af68 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sat, 4 Nov 2023 17:17:55 +0900 Subject: [PATCH 37/59] feat: snapshot --- .gitignore | 4 +- __init__.py | 161 ++- git_helper.py | 196 +++- js/a1111-alter-downloader.js | 554 ++++++++++ js/comfyui-manager.js | 1870 +++------------------------------ js/common.js | 62 ++ js/custom-nodes-downloader.js | 638 +++++++++++ js/model-downloader.js | 377 +++++++ js/snapshot.js | 279 +++++ prestartup_script.py | 119 ++- 10 files changed, 2518 insertions(+), 1742 deletions(-) create mode 100644 js/a1111-alter-downloader.js create mode 100644 js/common.js create mode 100644 js/custom-nodes-downloader.js create mode 100644 js/model-downloader.js create mode 100644 js/snapshot.js diff --git a/.gitignore b/.gitignore index fa803b09..4abc7136 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ __pycache__/ .idea/ .vscode/ .tmp -config.ini \ No newline at end of file +config.ini +snapshots/** +startup-scripts/** diff --git a/__init__.py b/__init__.py index 15ad8fba..89b8e01a 100644 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,8 @@ import folder_paths import os import sys import threading -import subprocess +import datetime +import re def handle_stream(stream, prefix): @@ -56,7 +57,7 @@ sys.path.append('../..') from torchvision.datasets.utils import download_url # ensure .js -print("### Loading: ComfyUI-Manager (V0.36.1)") +print("### Loading: ComfyUI-Manager (V0.37)") comfy_ui_required_revision = 1240 comfy_ui_revision = "Unknown" @@ -283,6 +284,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='') @@ -299,9 +308,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) @@ -312,8 +318,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 @@ -326,7 +335,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 @@ -352,11 +367,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() @@ -569,6 +590,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: @@ -663,6 +686,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: @@ -1073,6 +1215,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 @@ -1202,6 +1347,8 @@ async def channel_url_list(request): return web.Response(status=200) + WEB_DIRECTORY = "js" NODE_CLASS_MAPPINGS = {} __all__ = ['NODE_CLASS_MAPPINGS'] + diff --git a/git_helper.py b/git_helper.py index 5603fad1..2fbcf078 100644 --- a/git_helper.py +++ b/git_helper.py @@ -2,23 +2,39 @@ import sys import os import git import configparser +import re +import json +from torchvision.datasets.utils import download_url 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): + +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) + + 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 +64,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 +84,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 +104,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 +282,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) diff --git a/js/a1111-alter-downloader.js b/js/a1111-alter-downloader.js new file mode 100644 index 00000000..47bc28a8 --- /dev/null +++ b/js/a1111-alter-downloader.js @@ -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(`
Installing '${target.title}'`); + } + + 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 = ` ${data.custom_node.title}`; + } + 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); + } + } +} diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index 28ac95a0..239de383 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -1,7 +1,35 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js" import { ComfyDialog, $el } from "../../scripts/ui.js"; -import {ComfyWidgets} from "../../scripts/widgets.js"; +import { CustomNodesInstaller } from "./custom-nodes-downloader.js"; +import { AlternativesInstaller } from "./a1111-alter-downloader.js"; +import { SnapshotManager } from "./snapshot.js"; +import { ModelInstaller } from "./model-downloader.js"; +import { manager_instance, setManagerInstance } from "./common.js"; + +var style = document.createElement('style'); +style.innerHTML = ` +.cm-menu-container { + column-gap: 20px; + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.cm-menu-column { + display: flex; + flex-direction: column; +} + +.cm-title { + padding: 10px 10px 0 10p; + background-color: black; + text-align: center; + height: 45px; +} +`; + +document.head.appendChild(style); var update_comfyui_button = null; var fetch_updates_button = null; @@ -16,47 +44,7 @@ async function init_badge_mode() { await init_badge_mode(); -async function getCustomnodeMappings() { - var mode = "url"; - if(ManagerMenuDialog.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(ManagerMenuDialog.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 []; - } -} - -async function getCustomNodes() { - var mode = "url"; - if(ManagerMenuDialog.instance.local_mode_checkbox.checked) - mode = "local"; - - var skip_update = ""; - if(ManagerMenuDialog.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 fetchNicknames() { const response1 = await api.fetchApi(`/customnode/getmappings?mode=local`); @@ -84,85 +72,6 @@ async function fetchNicknames() { let nicknames = await fetchNicknames(); -async function getAlterList() { - var mode = "url"; - if(ManagerMenuDialog.instance.local_mode_checkbox.checked) - mode = "local"; - - var skip_update = ""; - if(ManagerMenuDialog.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; -} - -async function getModelList() { - var mode = "url"; - if(ManagerMenuDialog.instance.local_mode_checkbox.checked) - mode = "local"; - - const response = await api.fetchApi(`/externalmodel/getlist?mode=${mode}`); - - const data = await response.json(); - return data; -} - -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 = 9999; - continue; - } - - const status = await response.json(); - app.ui.dialog.close(); - target.installed = 'True'; - continue; - } - catch(exception) { - failed += `
${target.title}`; - } - } - - if(failed != '') { - app.ui.dialog.show(`${mode} failed: ${failed}`); - app.ui.dialog.element.style.zIndex = 9999; - } - - await caller.invalidateControl(); - caller.updateMessage('
To apply the installed/disabled/enabled custom node, please restart ComfyUI.'); - } -} async function updateComfyUI() { let prev_text = update_comfyui_button.innerText; @@ -210,7 +119,7 @@ async function fetchUpdates(update_check_checkbox) { try { var mode = "url"; - if(ManagerMenuDialog.instance.local_mode_checkbox.checked) + if(manager_instance.local_mode_checkbox.checked) mode = "local"; const response = await api.fetchApi(`/customnode/fetch_updates?mode=${mode}`); @@ -253,7 +162,7 @@ async function updateAll(update_check_checkbox) { try { var mode = "url"; - if(ManagerMenuDialog.instance.local_mode_checkbox.checked) + if(manager_instance.local_mode_checkbox.checked) mode = "local"; update_all_button.innerText = "Updating all..."; @@ -288,1514 +197,12 @@ async function updateAll(update_check_checkbox) { } } -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 = 9999; - return false; - } - finally { - await ModelInstaller.instance.invalidateControl(); - ModelInstaller.instance.updateMessage("
To apply the installed model, please click the 'Refresh' button on the main menu."); - } - } -} - - -// ----- -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(`
Installing '${target.title}'`); - } - - 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 = ` ${data.title}`; - 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"; - } - catch(exception) { - app.ui.dialog.show(`Failed to get custom node list. / ${exception}`); - } - } -} - -// ----- -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(`
Installing '${target.title}'`); - } - - 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 = ` ${data.custom_node.title}`; - } - 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"; - } - catch(exception) { - app.ui.dialog.show(`Failed to get alternatives list. / ${exception}`); - console.error(exception); - } - } -} - - -// ----------- -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(`
Installing '${target.name}'`); - - 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 = ` ${data.name}`; - 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"; - } - catch(exception) { - app.ui.dialog.show(`Failed to get external model list. / ${exception}`); - } - } -} - // ----------- class ManagerMenuDialog extends ComfyDialog { - static instance = null; local_mode_checkbox = null; - createButtons() { - this.local_mode_checkbox = $el("input",{type:'checkbox', id:"use_local_db"},[]) - const checkbox_text = $el("label",{},[" Use local DB"]) - checkbox_text.style.color = "var(--fg-color)"; - checkbox_text.style.marginRight = "10px"; - - this.update_check_checkbox = $el("input",{type:'checkbox', id:"skip_update_check"},[]) - const uc_checkbox_text = $el("label",{},[" Skip update check"]) - uc_checkbox_text.style.color = "var(--fg-color)"; - this.update_check_checkbox.checked = true; - + createControlsMid() { update_comfyui_button = $el("button", { type: "button", @@ -1820,7 +227,76 @@ class ManagerMenuDialog extends ComfyDialog { () => updateAll(this.update_check_checkbox) }); - // preview method + const res = + [ + $el("button", { + type: "button", + textContent: "Install Custom Nodes", + onclick: + () => { + if(!CustomNodesInstaller.instance) + CustomNodesInstaller.instance = new CustomNodesInstaller(app); + CustomNodesInstaller.instance.show(false); + } + }), + + $el("button", { + type: "button", + textContent: "Install Missing Custom Nodes", + onclick: + () => { + if(!CustomNodesInstaller.instance) + CustomNodesInstaller.instance = new CustomNodesInstaller(app); + CustomNodesInstaller.instance.show(true); + } + }), + + $el("button", { + type: "button", + textContent: "Install Models", + onclick: + () => { + if(!ModelInstaller.instance) + ModelInstaller.instance = new ModelInstaller(app); + ModelInstaller.instance.show(); + } + }), + + $el("br", {}, []), + update_all_button, + update_comfyui_button, + fetch_updates_button, + + $el("br", {}, []), + $el("button", { + type: "button", + textContent: "Alternatives of A1111", + onclick: + () => { + if(!AlternativesInstaller.instance) + AlternativesInstaller.instance = new AlternativesInstaller(app); + AlternativesInstaller.instance.show(); + } + }), + + $el("br", {}, []), + ]; + + return res; + } + + createControlsLeft() { + this.local_mode_checkbox = $el("input",{type:'checkbox', id:"use_local_db"},[]) + const checkbox_text = $el("label",{},[" Use local DB"]) + checkbox_text.style.color = "var(--fg-color)"; + checkbox_text.style.marginRight = "10px"; + + this.update_check_checkbox = $el("input",{type:'checkbox', id:"skip_update_check"},[]) + const uc_checkbox_text = $el("label",{},[" Skip update check"]) + uc_checkbox_text.style.color = "var(--fg-color)"; + this.update_check_checkbox.checked = true; + + // preview method let preview_combo = document.createElement("select"); preview_combo.appendChild($el('option', {value:'auto', text:'Preview method: Auto'}, [])); preview_combo.appendChild($el('option', {value:'taesd', text:'Preview method: TAESD (slow)'}, [])); @@ -1876,68 +352,37 @@ class ManagerMenuDialog extends ComfyDialog { } }); - const res = - [ - $el("tr.td", {width:"100%"}, [$el("font", {size:6, color:"white"}, [`ComfyUI Manager Menu`])]), - $el("br", {}, []), - $el("div", {}, [this.local_mode_checkbox, checkbox_text, this.update_check_checkbox, uc_checkbox_text]), - $el("br", {}, []), - $el("button", { - type: "button", - textContent: "Install Custom Nodes", - onclick: - () => { - if(!CustomNodesInstaller.instance) - CustomNodesInstaller.instance = new CustomNodesInstaller(app); - CustomNodesInstaller.instance.show(false); - } - }), + return [ + $el("div", {}, [this.local_mode_checkbox, checkbox_text, this.update_check_checkbox, uc_checkbox_text]), + $el("br", {}, []), + preview_combo, + badge_combo, + channel_combo, - $el("button", { - type: "button", - textContent: "Install Missing Custom Nodes", - onclick: - () => { - if(!CustomNodesInstaller.instance) - CustomNodesInstaller.instance = new CustomNodesInstaller(app); - CustomNodesInstaller.instance.show(true); - } - }), + $el("hr", {}, []), + $el("center", {}, ["!! EXPERIMENTAL !!"]), + $el("br", {}, []), + $el("button", { + type: "button", + textContent: "Snapshot Manager", + onclick: + () => { + if(!SnapshotManager.instance) + SnapshotManager.instance = new SnapshotManager(app); + SnapshotManager.instance.show(); + } + }), + ]; + } - $el("button", { - type: "button", - textContent: "Install Models", - onclick: - () => { - if(!ModelInstaller.instance) - ModelInstaller.instance = new ModelInstaller(app); - ModelInstaller.instance.show(); - } - }), - - $el("br", {}, []), - update_all_button, - update_comfyui_button, - fetch_updates_button, - - $el("br", {}, []), - $el("button", { - type: "button", - textContent: "Alternatives of A1111", - onclick: - () => { - if(!AlternativesInstaller.instance) - AlternativesInstaller.instance = new AlternativesInstaller(app); - AlternativesInstaller.instance.show(); - } - }), - - $el("br", {}, []), + createControlsRight() { + return [ $el("button", { type: "button", textContent: "ComfyUI Community Manual", onclick: () => { window.open("https://blenderneko.github.io/ComfyUI-docs/", "comfyui-community-manual"); } }), + $el("button", { type: "button", textContent: "ComfyUI Workflow Gallery", @@ -1949,36 +394,41 @@ class ManagerMenuDialog extends ComfyDialog { textContent: "ComfyUI Nodes Info", onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); } }), - - $el("br", {}, []), - $el("hr", {width: "100%"}, []), - preview_combo, - badge_combo, - channel_combo, - $el("hr", {width: "100%"}, []), - $el("br", {}, []), - - $el("button", { - type: "button", - textContent: "Close", - onclick: () => this.close() - }), - $el("br", {}, []), - ]; - - res[0].style.padding = "10px 10px 0 10px"; - res[0].style.backgroundColor = "black"; - res[0].style.textAlign = "center"; - res[0].style.height = "45px"; - return res; + ]; } constructor() { super(); - this.element = $el("div.comfy-modal", { parent: document.body }, - [ $el("div.comfy-modal-content", - [...this.createButtons()]), - ]); + + const close_button = $el("button", { type: "button", textContent: "Close", onclick: () => this.close() }); + close_button.style.position = "absolute"; + close_button.style.bottom = "20px"; + close_button.style.width = "calc(100% - 60px)"; + + const content = + $el("div.comfy-modal-content", + [ + $el("tr.cm-title", {width:"100%"}, [ + $el("font", {size:6, color:"white"}, [`ComfyUI Manager Menu`])] + ), + $el("br", {}, []), + $el("div.cm-menu-container", + [ + $el("div.cm-menu-column", [...this.createControlsLeft()]), + $el("div.cm-menu-column", [...this.createControlsMid()]), + $el("div.cm-menu-column", [...this.createControlsRight()]) + ]), + close_button, + ] + ); + + content.style.width = '100%'; + content.style.height = '100%'; + + this.element = $el("div.comfy-modal", { parent: document.body }, [ content ]); + this.element.style.width = '1000px'; + this.element.style.height = '400px'; + this.element.style.zIndex = 10000; } show() { @@ -2000,9 +450,9 @@ app.registerExtension({ const managerButton = document.createElement("button"); managerButton.textContent = "Manager"; managerButton.onclick = () => { - if(!ManagerMenuDialog.instance) - ManagerMenuDialog.instance = new ManagerMenuDialog(); - ManagerMenuDialog.instance.show(); + if(!manager_instance) + setManagerInstance(new ManagerMenuDialog()); + manager_instance.show(); } menu.append(managerButton); }, diff --git a/js/common.js b/js/common.js new file mode 100644 index 00000000..b5562691 --- /dev/null +++ b/js/common.js @@ -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 += `
${target.title}`; + } + } + + if(failed != '') { + app.ui.dialog.show(`${mode} failed: ${failed}`); + app.ui.dialog.element.style.zIndex = 10010; + } + + await caller.invalidateControl(); + caller.updateMessage('
To apply the installed/disabled/enabled custom node, please restart ComfyUI.'); + } +}; + +export var manager_instance = null; + +export function setManagerInstance(obj) { + manager_instance = obj; +} \ No newline at end of file diff --git a/js/custom-nodes-downloader.js b/js/custom-nodes-downloader.js new file mode 100644 index 00000000..fd8553e5 --- /dev/null +++ b/js/custom-nodes-downloader.js @@ -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(`
Installing '${target.title}'`); + } + + 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 = ` ${data.title}`; + 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}`); + } + } +} \ No newline at end of file diff --git a/js/model-downloader.js b/js/model-downloader.js new file mode 100644 index 00000000..c83950ff --- /dev/null +++ b/js/model-downloader.js @@ -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("
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(`
Installing '${target.name}'`); + + 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 = ` ${data.name}`; + 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}`); + } + } +} diff --git a/js/snapshot.js b/js/snapshot.js new file mode 100644 index 00000000..ff009543 --- /dev/null +++ b/js/snapshot.js @@ -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("
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("
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(`
Restore snapshot '${target.name}'`); + + 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}`); + } + } +} diff --git a/prestartup_script.py b/prestartup_script.py index 43d7c862..374e62ba 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -17,6 +17,35 @@ 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): + for msg in stream: + 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 +125,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 +140,66 @@ 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): + for msg in stream: + if msg.startswith("CLONE: "): + cloned_repos.append(msg[7:]) + + 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) + + 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] + 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 +210,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 +219,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}") From e497b8321f2face82d3f1491a4c3e05834703d76 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sat, 4 Nov 2023 21:23:29 +0900 Subject: [PATCH 38/59] feat: tqdm for clone fix: snapshot restore proper working directory --- __init__.py | 19 ++++++++++++++++--- git_helper.py | 16 +++++++++++++++- prestartup_script.py | 2 ++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index 89b8e01a..df8d8e19 100644 --- a/__init__.py +++ b/__init__.py @@ -6,7 +6,8 @@ import sys import threading import datetime import re - +from tqdm.auto import tqdm +from git.remote import RemoteProgress def handle_stream(stream, prefix): for line in stream: @@ -57,7 +58,7 @@ sys.path.append('../..') from torchvision.datasets.utils import download_url # ensure .js -print("### Loading: ComfyUI-Manager (V0.37)") +print("### Loading: ComfyUI-Manager (V0.38)") comfy_ui_required_revision = 1240 comfy_ui_revision = "Unknown" @@ -952,6 +953,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: @@ -966,7 +979,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() diff --git a/git_helper.py b/git_helper.py index 2fbcf078..73bf9c81 100644 --- a/git_helper.py +++ b/git_helper.py @@ -5,18 +5,32 @@ 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() +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(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}]") diff --git a/prestartup_script.py b/prestartup_script.py index 374e62ba..b7a63cc0 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -163,6 +163,7 @@ if os.path.exists(restore_snapshot_path): 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') @@ -179,6 +180,7 @@ if os.path.exists(restore_snapshot_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: From a51091cf7e9d6da23ad13458687143af3d87391e Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sat, 4 Nov 2023 21:42:22 +0900 Subject: [PATCH 39/59] update README.md update DB --- README.md | 12 ++++++++++++ extension-node-map.json | 6 ++++++ misc/menu.jpg | Bin 53704 -> 69313 bytes misc/snapshot.jpg | Bin 0 -> 92776 bytes node_db/dev/custom-node-list.json | 10 ++++++++++ node_db/new/extension-node-map.json | 6 ++++++ 6 files changed, 34 insertions(+) create mode 100644 misc/snapshot.jpg diff --git a/README.md b/README.md index b3a23170..9f074a7e 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,18 @@ This repository provides Colab notebooks that allow you to install and use Comfy ![model-install-dialog](misc/nickname.jpg) + +## 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. + + +![model-install-dialog](misc/snapshot.jpg) + ## 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. diff --git a/extension-node-map.json b/extension-node-map.json index b089fa19..7fc4cf46 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -1274,9 +1274,14 @@ "ExtractOpticalFlow", "LoadFrame", "LoadFrameFromDataset", + "LoadFrameFromFolder", + "LoadFramePairFromDataset", "LoadFrameSequence", "MakeFrameDataset", + "MixConsistencyMaps", "OffsetNumber", + "ResizeToFit", + "SaveFrame", "WarpFrame" ], { @@ -2803,6 +2808,7 @@ "GetImageRangeFromBatch", "GrowMaskWithBlur", "INTConstant", + "SaveImageWithAlpha", "SomethingToString", "VRAM_Debug" ], diff --git a/misc/menu.jpg b/misc/menu.jpg index 64c816e72241c2db8ceade7a0acaa5ee88721464..e9e284cd18e606b7efff40cff447b0445ea905bb 100644 GIT binary patch literal 69313 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 literal 53704 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 VkjK6D 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 Tb)4eJv=+=-E_yTmeg3}yrlUpN diff --git a/misc/snapshot.jpg b/misc/snapshot.jpg new file mode 100644 index 0000000000000000000000000000000000000000..33269564bbd2b286994c78a92bf09905d251907b GIT binary patch literal 92776 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 WD^C9rj Date: Sat, 4 Nov 2023 21:45:52 +0900 Subject: [PATCH 40/59] update README --- README.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9f074a7e..750462f2 100644 --- a/README.md +++ b/README.md @@ -169,10 +169,6 @@ NODE_CLASS_MAPPINGS.update({ ![missing-list](misc/missing-list.png) -* 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. @@ -194,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 -- [ ] installation from git url -- [ ] Specification of custom nodes -- [ ] Specification scanner -- [ ] Search extension by node name -- [ ] workflow downloader +- [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 # Disclaimer From 458f1f6817eead96f295ece01feb4d44ea4f62f8 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 5 Nov 2023 00:44:52 +0900 Subject: [PATCH 41/59] fix: exception handling for handle_stream --- __init__.py | 10 +++++++--- prestartup_script.py | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/__init__.py b/__init__.py index df8d8e19..d59cdfcb 100644 --- a/__init__.py +++ b/__init__.py @@ -9,9 +9,13 @@ import re from tqdm.auto import tqdm from git.remote import RemoteProgress + def handle_stream(stream, prefix): - for line in stream: - print(prefix, line, end="") + try: + for line in stream: + print(prefix, line, end="") + except Exception: + print("[!] ??? log decoding error ???") def run_script(cmd, cwd='.'): @@ -58,7 +62,7 @@ sys.path.append('../..') from torchvision.datasets.utils import download_url # ensure .js -print("### Loading: ComfyUI-Manager (V0.38)") +print("### Loading: ComfyUI-Manager (V0.38.1)") comfy_ui_required_revision = 1240 comfy_ui_revision = "Unknown" diff --git a/prestartup_script.py b/prestartup_script.py index b7a63cc0..f80c1a36 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -25,8 +25,11 @@ git_script_path = os.path.join(comfyui_manager_path, "git_helper.py") def handle_stream(stream, prefix): - for msg in stream: - print(prefix, msg, end="") + try: + for msg in stream: + print(prefix, msg, end="") + except Exception: + print("[!] ??? log decoding error ???") def process_wrap(cmd_str, cwd_path, handler=None): @@ -147,11 +150,14 @@ if os.path.exists(restore_snapshot_path): cloned_repos = [] def msg_capture(stream, prefix): - for msg in stream: - if msg.startswith("CLONE: "): - cloned_repos.append(msg[7:]) + try: + for msg in stream: + if msg.startswith("CLONE: "): + cloned_repos.append(msg[7:]) - print(prefix, msg, end="") + print(prefix, msg, end="") + except Exception: + print("[!] [snapshot restore] ??? log decoding error ???") print(f"[ComfyUI-Manager] Restore snapshot.") cmd_str = [sys.executable, git_script_path, '--apply-snapshot', restore_snapshot_path] From 74e1f6247024c8f143577bad7a948ee3a760ce01 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 5 Nov 2023 02:20:30 +0900 Subject: [PATCH 42/59] update DB --- extension-node-map.json | 7 +++++++ node_db/new/extension-node-map.json | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/extension-node-map.json b/extension-node-map.json index 7fc4cf46..6ac6e81b 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -2808,6 +2808,7 @@ "GetImageRangeFromBatch", "GrowMaskWithBlur", "INTConstant", + "ReverseImageBatch", "SaveImageWithAlpha", "SomethingToString", "VRAM_Debug" @@ -2966,6 +2967,7 @@ "ImpactEdit_SEG_ELT", "ImpactFloat", "ImpactFrom_SEG_ELT", + "ImpactHFTransformersClassifierProvider", "ImpactImageBatchToImageList", "ImpactImageInfo", "ImpactInt", @@ -2980,6 +2982,7 @@ "ImpactNeg", "ImpactNodeSetMuteState", "ImpactQueueTrigger", + "ImpactSEGSClassify", "ImpactSEGSConcat", "ImpactSEGSLabelFilter", "ImpactSEGSOrderedFilter", @@ -3204,6 +3207,8 @@ "Batch Float Assemble (mtb)", "Batch Float Fill (mtb)", "Batch Make (mtb)", + "Batch Merge (mtb)", + "Batch Shake (mtb)", "Batch Shape (mtb)", "Batch Transform (mtb)", "Bbox (mtb)", @@ -3237,6 +3242,7 @@ "Load Image From Url (mtb)", "Load Image Sequence (mtb)", "Mask To Image (mtb)", + "Math Expression (mtb)", "Model Patch Seamless (mtb)", "Qr Code (mtb)", "Restore Face (mtb)", @@ -3244,6 +3250,7 @@ "Save Image Grid (mtb)", "Save Image Sequence (mtb)", "Save Tensors (mtb)", + "Sharpen (mtb)", "Smart Step (mtb)", "Stack Images (mtb)", "String Replace (mtb)", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 7fc4cf46..6ac6e81b 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -2808,6 +2808,7 @@ "GetImageRangeFromBatch", "GrowMaskWithBlur", "INTConstant", + "ReverseImageBatch", "SaveImageWithAlpha", "SomethingToString", "VRAM_Debug" @@ -2966,6 +2967,7 @@ "ImpactEdit_SEG_ELT", "ImpactFloat", "ImpactFrom_SEG_ELT", + "ImpactHFTransformersClassifierProvider", "ImpactImageBatchToImageList", "ImpactImageInfo", "ImpactInt", @@ -2980,6 +2982,7 @@ "ImpactNeg", "ImpactNodeSetMuteState", "ImpactQueueTrigger", + "ImpactSEGSClassify", "ImpactSEGSConcat", "ImpactSEGSLabelFilter", "ImpactSEGSOrderedFilter", @@ -3204,6 +3207,8 @@ "Batch Float Assemble (mtb)", "Batch Float Fill (mtb)", "Batch Make (mtb)", + "Batch Merge (mtb)", + "Batch Shake (mtb)", "Batch Shape (mtb)", "Batch Transform (mtb)", "Bbox (mtb)", @@ -3237,6 +3242,7 @@ "Load Image From Url (mtb)", "Load Image Sequence (mtb)", "Mask To Image (mtb)", + "Math Expression (mtb)", "Model Patch Seamless (mtb)", "Qr Code (mtb)", "Restore Face (mtb)", @@ -3244,6 +3250,7 @@ "Save Image Grid (mtb)", "Save Image Sequence (mtb)", "Save Tensors (mtb)", + "Sharpen (mtb)", "Smart Step (mtb)", "Stack Images (mtb)", "String Replace (mtb)", From 94cb2f808de26505ac1be0d091b00f1dcd402f43 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 5 Nov 2023 02:32:30 +0900 Subject: [PATCH 43/59] fix: add snapshots dir placeholder --- snapshots/the_snapshot_files_are_located_here | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 snapshots/the_snapshot_files_are_located_here diff --git a/snapshots/the_snapshot_files_are_located_here b/snapshots/the_snapshot_files_are_located_here new file mode 100644 index 00000000..e69de29b From 2b18d4f0ed0f4ecfe3b08f7189d18e190b18a037 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 5 Nov 2023 02:33:17 +0900 Subject: [PATCH 44/59] update version --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index d59cdfcb..1c148fef 100644 --- a/__init__.py +++ b/__init__.py @@ -62,7 +62,7 @@ sys.path.append('../..') from torchvision.datasets.utils import download_url # ensure .js -print("### Loading: ComfyUI-Manager (V0.38.1)") +print("### Loading: ComfyUI-Manager (V0.38.2)") comfy_ui_required_revision = 1240 comfy_ui_revision = "Unknown" From c78c7d7e8fbe86d175d45551926f76a31f572e72 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 5 Nov 2023 09:00:14 +0900 Subject: [PATCH 45/59] fix: failed to ensure gitpython on startup https://github.com/ltdrdata/ComfyUI-Manager/discussions/163 --- __init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 1c148fef..cd244146 100644 --- a/__init__.py +++ b/__init__.py @@ -7,7 +7,6 @@ import threading import datetime import re from tqdm.auto import tqdm -from git.remote import RemoteProgress def handle_stream(stream, prefix): @@ -57,12 +56,14 @@ 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.38.2)") +print("### Loading: ComfyUI-Manager (V0.38.3)") comfy_ui_required_revision = 1240 comfy_ui_revision = "Unknown" From 92ce2d706b5c29cd0646a26d765a7d4a9a98b1c6 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 5 Nov 2023 10:05:39 +0900 Subject: [PATCH 46/59] improve: tqdm in process fix: encoding error of process output --- __init__.py | 12 ++++++------ prestartup_script.py | 43 +++++++++++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/__init__.py b/__init__.py index cd244146..f50c4620 100644 --- a/__init__.py +++ b/__init__.py @@ -6,15 +6,15 @@ import sys import threading import datetime import re +import locale from tqdm.auto import tqdm +version = "V0.39" def handle_stream(stream, prefix): - try: - for line in stream: - print(prefix, line, end="") - except Exception: - print("[!] ??? log decoding error ???") + stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace') + for line in stream: + print(prefix, line, end="") def run_script(cmd, cwd='.'): @@ -63,7 +63,7 @@ sys.path.append('../..') from torchvision.datasets.utils import download_url # ensure .js -print("### Loading: ComfyUI-Manager (V0.38.3)") +print(f"### Loading: ComfyUI-Manager ({version})") comfy_ui_required_revision = 1240 comfy_ui_revision = "Unknown" diff --git a/prestartup_script.py b/prestartup_script.py index f80c1a36..65053133 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -5,6 +5,7 @@ import sys import atexit import threading import re +import locale message_collapses = [] @@ -25,11 +26,18 @@ git_script_path = os.path.join(comfyui_manager_path, "git_helper.py") def handle_stream(stream, prefix): - try: - for msg in stream: - print(prefix, msg, end="") - except Exception: - print("[!] ??? log decoding error ???") + stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace') + for msg in stream: + if prefix == '[!]' and ('it/s]' 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): @@ -150,14 +158,25 @@ if os.path.exists(restore_snapshot_path): cloned_repos = [] def msg_capture(stream, prefix): - try: - for msg in stream: - if msg.startswith("CLONE: "): - cloned_repos.append(msg[7:]) + 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="") - print(prefix, msg, end="") - except Exception: - print("[!] [snapshot restore] ??? log decoding error ???") + 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] From 97c8d204d23fef1a89116f7b3135e13cbac9a4a8 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 5 Nov 2023 10:28:11 +0900 Subject: [PATCH 47/59] update DB --- __init__.py | 1 + custom-node-list.json | 10 ++++++++++ extension-node-map.json | 19 +++++++++++++++++++ node_db/new/custom-node-list.json | 10 ++++++++++ node_db/new/extension-node-map.json | 19 +++++++++++++++++++ 5 files changed, 59 insertions(+) diff --git a/__init__.py b/__init__.py index f50c4620..a4b35420 100644 --- a/__init__.py +++ b/__init__.py @@ -11,6 +11,7 @@ from tqdm.auto import tqdm version = "V0.39" + def handle_stream(stream, prefix): stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace') for line in stream: diff --git a/custom-node-list.json b/custom-node-list.json index 10493209..16b6351b 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -2662,6 +2662,16 @@ "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": "Smuzzies", "title": "Chatbox Overlay node for ComfyUI", diff --git a/extension-node-map.json b/extension-node-map.json index 6ac6e81b..6a4760ed 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -477,6 +477,7 @@ "BatchPromptSchedule", "BatchPromptScheduleEncodeSDXL", "BatchPromptScheduleLatentInput", + "BatchPromptScheduleNodeFlowEnd", "BatchPromptScheduleSDXLLatentInput", "BatchStringSchedule", "BatchValueSchedule", @@ -930,6 +931,23 @@ "title_aux": "QRNG_Node_ComfyUI" } ], + "https://github.com/PCMonsterx/ComfyUI-CSV-Loader": [ + [ + "Load Artists CSV", + "Load Artmovements CSV", + "Load Characters CSV", + "Load Colors CSV", + "Load Composition CSV", + "Load Lighting CSV", + "Load Negative CSV", + "Load Positive CSV", + "Load Settings CSV", + "Load Styles CSV" + ], + { + "title_aux": "ComfyUI-CSV-Loader" + } + ], "https://github.com/ParmanBabra/ComfyUI-Malefish-Custom-Scripts": [ [ "CSVPromptsLoader", @@ -3599,6 +3617,7 @@ "AV_ControlNetLoader", "AV_ControlNetPreprocessor", "AV_LoraListLoader", + "AV_LoraListStacker", "AV_LoraLoader", "AV_ParametersPipeToCheckpointModels", "AV_ParametersPipeToPrompts", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index c7ce7fe0..e5cf4cc1 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 6ac6e81b..6a4760ed 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -477,6 +477,7 @@ "BatchPromptSchedule", "BatchPromptScheduleEncodeSDXL", "BatchPromptScheduleLatentInput", + "BatchPromptScheduleNodeFlowEnd", "BatchPromptScheduleSDXLLatentInput", "BatchStringSchedule", "BatchValueSchedule", @@ -930,6 +931,23 @@ "title_aux": "QRNG_Node_ComfyUI" } ], + "https://github.com/PCMonsterx/ComfyUI-CSV-Loader": [ + [ + "Load Artists CSV", + "Load Artmovements CSV", + "Load Characters CSV", + "Load Colors CSV", + "Load Composition CSV", + "Load Lighting CSV", + "Load Negative CSV", + "Load Positive CSV", + "Load Settings CSV", + "Load Styles CSV" + ], + { + "title_aux": "ComfyUI-CSV-Loader" + } + ], "https://github.com/ParmanBabra/ComfyUI-Malefish-Custom-Scripts": [ [ "CSVPromptsLoader", @@ -3599,6 +3617,7 @@ "AV_ControlNetLoader", "AV_ControlNetPreprocessor", "AV_LoraListLoader", + "AV_LoraListStacker", "AV_LoraLoader", "AV_ParametersPipeToCheckpointModels", "AV_ParametersPipeToPrompts", From a05c483c4538bd0dc2a6046998afa2038576d486 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sun, 5 Nov 2023 11:14:10 +0900 Subject: [PATCH 48/59] refactor: move version printer to the top --- __init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index a4b35420..feb0abee 100644 --- a/__init__.py +++ b/__init__.py @@ -9,7 +9,9 @@ import re import locale from tqdm.auto import tqdm -version = "V0.39" + +version = "V0.39.1" +print(f"### Loading: ComfyUI-Manager ({version})") def handle_stream(stream, prefix): @@ -63,9 +65,6 @@ sys.path.append('../..') from torchvision.datasets.utils import download_url -# ensure .js -print(f"### Loading: ComfyUI-Manager ({version})") - comfy_ui_required_revision = 1240 comfy_ui_revision = "Unknown" From ec61a3029f8c97f40b078d43ca4bc6238cea9355 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" <128333288+ltdrdata@users.noreply.github.com> Date: Mon, 6 Nov 2023 08:45:14 +0900 Subject: [PATCH 49/59] Update __init__.py fix: missing import subprocess --- __init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index feb0abee..1d7d0b17 100644 --- a/__init__.py +++ b/__init__.py @@ -7,10 +7,11 @@ import threading import datetime import re import locale +import subprocess # don't remove this from tqdm.auto import tqdm -version = "V0.39.1" +version = "V0.39.2" print(f"### Loading: ComfyUI-Manager ({version})") From 332e4e285606b6dcb08cc5ce5395453c4cbd47fb Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Mon, 6 Nov 2023 09:57:30 +0900 Subject: [PATCH 50/59] fix: git_helper's tqdm encoding error --- __init__.py | 15 ++++++++++++--- git_helper.py | 2 +- prestartup_script.py | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index 1d7d0b17..3bad7ef0 100644 --- a/__init__.py +++ b/__init__.py @@ -11,14 +11,23 @@ import subprocess # don't remove this from tqdm.auto import tqdm -version = "V0.39.2" +version = "V0.39.3" print(f"### Loading: ComfyUI-Manager ({version})") def handle_stream(stream, prefix): stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace') - for line in stream: - print(prefix, line, end="") + 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='.'): diff --git a/git_helper.py b/git_helper.py index 73bf9c81..504b5e07 100644 --- a/git_helper.py +++ b/git_helper.py @@ -16,7 +16,7 @@ working_directory = os.getcwd() class GitProgress(RemoteProgress): def __init__(self): super().__init__() - self.pbar = tqdm() + self.pbar = tqdm(ascii=True) def update(self, op_code, cur_count, max_count=None, message=''): self.pbar.total = max_count diff --git a/prestartup_script.py b/prestartup_script.py index 65053133..4c4054a4 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -28,7 +28,7 @@ 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]' or 's/it]') in msg and ('%|' in msg or 'it [' in msg): + 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: From 3472eeb2827a514519979d3aaeca6ee05674b2d4 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Mon, 6 Nov 2023 11:15:36 +0900 Subject: [PATCH 51/59] update DB --- custom-node-list.json | 1 + 1 file changed, 1 insertion(+) diff --git a/custom-node-list.json b/custom-node-list.json index 16b6351b..2f7a5e93 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -1451,6 +1451,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." }, From 214197a24d0dca84e0714466865b6189d1b19979 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Mon, 6 Nov 2023 22:35:18 +0900 Subject: [PATCH 52/59] update DB improve: scanner.py - parallel pull --- custom-node-list.json | 20 ++++++++++++++++++++ extension-node-map.json | 28 +++++++++++++++++++++++++--- node_db/new/custom-node-list.json | 20 ++++++++++++++++++++ node_db/new/extension-node-map.json | 28 +++++++++++++++++++++++++--- scanner.py | 10 ++++++++-- 5 files changed, 98 insertions(+), 8 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 2f7a5e93..0570f2b6 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -160,6 +160,16 @@ "install_type": "git-clone", "description": "A collection of ComfyUI custom nodes to help streamline workflows and reduce total node count.

NOTE: This node is originally created by LucianoCirino, but the original repository is no longer maintained and has been forked by a new maintainer. To use the forked version, you should uninstall the original version and REINSTALL this one.

" }, + { + "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", "title": "Derfuu_ComfyUI_ModdedNodes", @@ -2673,6 +2683,16 @@ "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", diff --git a/extension-node-map.json b/extension-node-map.json index 6a4760ed..1a73b951 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -709,6 +709,7 @@ "Checkpoint", "Float", "Int", + "Lora", "Operation", "String" ], @@ -1436,6 +1437,14 @@ "title_aux": "YARS: Yet Another Resolution Selector" } ], + "https://github.com/Trung0246/ComfyUI-0246": [ + [ + "Highway" + ], + { + "title_aux": "ComfyUI-0246" + } + ], "https://github.com/Ttl/ComfyUi_NNLatentUpscale": [ [ "NNLatentUpscale" @@ -2752,6 +2761,14 @@ "title_aux": "FaceSwap" } ], + "https://github.com/jags111/ComfyUI_Jags_VectorMagic": [ + [ + "my unique name" + ], + { + "title_aux": "ComfyUI_Jags_VectorMagic" + } + ], "https://github.com/jags111/efficiency-nodes-comfyui": [ [ "AnimateDiff Script", @@ -2826,6 +2843,11 @@ "GetImageRangeFromBatch", "GrowMaskWithBlur", "INTConstant", + "ImageBatchTestPattern", + "ImageConcanate", + "ImageGridComposite2x2", + "ImageGridComposite3x3", + "ReplaceImagesInBatch", "ReverseImageBatch", "SaveImageWithAlpha", "SomethingToString", @@ -3026,9 +3048,9 @@ "LatentReceiver", "LatentSender", "LatentSwitch", - "LoadConditioning", "MMDetDetectorProvider", "MMDetLoader", + "MaskDetailerPipe", "MaskListToMaskBatch", "MaskPainter", "MaskToSEGS", @@ -3058,7 +3080,6 @@ "SEGSPreview", "SEGSSwitch", "SEGSToImageList", - "SaveConditioning", "SegmDetectorCombined", "SegmDetectorCombined_v2", "SegmDetectorForEach", @@ -3176,7 +3197,8 @@ "DiscordServing", "ServingInputNumber", "ServingInputText", - "ServingOutput" + "ServingOutput", + "WebSocketServing" ], { "title_aux": "ComfyUI Serving toolkit" diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index e5cf4cc1..27c814b1 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,25 @@ { "custom_nodes": [ + { + "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", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 6a4760ed..1a73b951 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -709,6 +709,7 @@ "Checkpoint", "Float", "Int", + "Lora", "Operation", "String" ], @@ -1436,6 +1437,14 @@ "title_aux": "YARS: Yet Another Resolution Selector" } ], + "https://github.com/Trung0246/ComfyUI-0246": [ + [ + "Highway" + ], + { + "title_aux": "ComfyUI-0246" + } + ], "https://github.com/Ttl/ComfyUi_NNLatentUpscale": [ [ "NNLatentUpscale" @@ -2752,6 +2761,14 @@ "title_aux": "FaceSwap" } ], + "https://github.com/jags111/ComfyUI_Jags_VectorMagic": [ + [ + "my unique name" + ], + { + "title_aux": "ComfyUI_Jags_VectorMagic" + } + ], "https://github.com/jags111/efficiency-nodes-comfyui": [ [ "AnimateDiff Script", @@ -2826,6 +2843,11 @@ "GetImageRangeFromBatch", "GrowMaskWithBlur", "INTConstant", + "ImageBatchTestPattern", + "ImageConcanate", + "ImageGridComposite2x2", + "ImageGridComposite3x3", + "ReplaceImagesInBatch", "ReverseImageBatch", "SaveImageWithAlpha", "SomethingToString", @@ -3026,9 +3048,9 @@ "LatentReceiver", "LatentSender", "LatentSwitch", - "LoadConditioning", "MMDetDetectorProvider", "MMDetLoader", + "MaskDetailerPipe", "MaskListToMaskBatch", "MaskPainter", "MaskToSEGS", @@ -3058,7 +3080,6 @@ "SEGSPreview", "SEGSSwitch", "SEGSToImageList", - "SaveConditioning", "SegmDetectorCombined", "SegmDetectorCombined_v2", "SegmDetectorForEach", @@ -3176,7 +3197,8 @@ "DiscordServing", "ServingInputNumber", "ServingInputText", - "ServingOutput" + "ServingOutput", + "WebSocketServing" ], { "title_aux": "ComfyUI Serving toolkit" diff --git a/scanner.py b/scanner.py index 1755cb27..936a1c14 100644 --- a/scanner.py +++ b/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,14 +163,19 @@ 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] - + node_info[name] = (url, title) clone_or_pull_git_repository(url) + max_threads = 10 + with concurrent.futures.ThreadPoolExecutor(max_threads) 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: From e532dbca007041c0e2d534e1f7e7ac10983473e6 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Mon, 6 Nov 2023 22:43:21 +0900 Subject: [PATCH 53/59] improve: parallel fetch & update --- __init__.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/__init__.py b/__init__.py index 3bad7ef0..498f9d94 100644 --- a/__init__.py +++ b/__init__.py @@ -9,9 +9,9 @@ import re import locale import subprocess # don't remove this from tqdm.auto import tqdm +import concurrent - -version = "V0.39.3" +version = "V0.40" print(f"### Loading: ComfyUI-Manager ({version})") @@ -554,9 +554,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: @@ -677,10 +681,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): @@ -688,6 +691,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): From 06286426c0150f648e270b81b56c5c98821af5b8 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Mon, 6 Nov 2023 23:16:00 +0900 Subject: [PATCH 54/59] improve: scanner.py - parallel downalod update DB --- custom-node-list.json | 6 +++--- extension-node-map.json | 2 +- node_db/new/custom-node-list.json | 10 ++++++++++ node_db/new/extension-node-map.json | 2 +- scanner.py | 9 ++++++--- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 0570f2b6..280dca44 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -1606,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: Birch-san/diffusers-play/approx_vae. This ComfyUI node allows you to quickly preview SDXL 1.0 latents." }, { "author": "bradsec", diff --git a/extension-node-map.json b/extension-node-map.json index 1a73b951..e5981245 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -3351,7 +3351,7 @@ "FastLatentToImage" ], { - "title_aux": "ComfyUI-TeaNodes" + "title_aux": "ComfyUI_FastVAEDecorder_SDXL" } ], "https://github.com/nicolai256/comfyUI_Nodes_nicolai256/raw/main/yugioh-presets.py": [ diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 27c814b1..3a6fba72 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "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: Birch-san/diffusers-play/approx_vae. This ComfyUI node allows you to quickly preview SDXL 1.0 latents." + }, { "author": "jags111", "title": "ComfyUI_Jags_VectorMagic", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 1a73b951..e5981245 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -3351,7 +3351,7 @@ "FastLatentToImage" ], { - "title_aux": "ComfyUI-TeaNodes" + "title_aux": "ComfyUI_FastVAEDecorder_SDXL" } ], "https://github.com/nicolai256/comfyUI_Nodes_nicolai256/raw/main/yugioh-presets.py": [ diff --git a/scanner.py b/scanner.py index 936a1c14..37664b9a 100644 --- a/scanner.py +++ b/scanner.py @@ -171,14 +171,14 @@ def update_custom_nodes(): node_info[name] = (url, title) clone_or_pull_git_repository(url) - max_threads = 10 - with concurrent.futures.ThreadPoolExecutor(max_threads) as executor: + 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) @@ -187,6 +187,9 @@ def update_custom_nodes(): download_url(url, ".tmp") 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 From 012e2f077d481982177b4f2a848ce4827bd9ca88 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Tue, 7 Nov 2023 00:17:14 +0900 Subject: [PATCH 55/59] update DB --- extension-node-map.json | 2 ++ node_db/new/extension-node-map.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/extension-node-map.json b/extension-node-map.json index e5981245..3ef06e0d 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -1319,6 +1319,8 @@ ], "https://github.com/THtianhao/ComfyUI-FaceChain": [ [ + "FC_FaceDetection", + "FC_FaceFusion", "FC_LoraMerge" ], { diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index e5981245..3ef06e0d 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -1319,6 +1319,8 @@ ], "https://github.com/THtianhao/ComfyUI-FaceChain": [ [ + "FC_FaceDetection", + "FC_FaceFusion", "FC_LoraMerge" ], { From 1b8e4f8d37f9e1acf86476d4e2e6210cb4d326d7 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Tue, 7 Nov 2023 07:45:11 +0900 Subject: [PATCH 56/59] update DB --- custom-node-list.json | 10 +++ extension-node-map.json | 14 ++++ node_db/new/custom-node-list.json | 101 +++------------------------- node_db/new/extension-node-map.json | 14 ++++ 4 files changed, 48 insertions(+), 91 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 280dca44..98080067 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -600,6 +600,16 @@ "install_type": "git-clone", "description": "Nodes: Plasma Noise, Random Noise, Greyscale Noise, Pink Noise, Brown Noise, Plasma KSampler" }, + { + "author": "Jordach", + "title": "comfy-consistency-vae", + "reference": "https://github.com/Jordach/comfy-consistency-vae", + "files": [ + "https://github.com/Jordach/comfy-consistency-vae" + ], + "install_type": "git-clone", + "description": "Nodes: Comfy_ConsistencyVAE" + }, { "author": "bvhari", "title": "ImageProcessing", diff --git a/extension-node-map.json b/extension-node-map.json index 3ef06e0d..3d32db6a 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -586,6 +586,14 @@ "title_aux": "Rembg Background Removal Node for ComfyUI" } ], + "https://github.com/Jordach/comfy-consistency-vae": [ + [ + "Comfy_ConsistencyVAE" + ], + { + "title_aux": "comfy-consistency-vae" + } + ], "https://github.com/Jordach/comfy-plasma": [ [ "JDC_AutoContrast", @@ -3897,13 +3905,19 @@ "LCMGenerate", "LCMGenerate_ReferenceOnly", "LCMGenerate_img2img", + "LCMGenerate_img2img_IPAdapter", "LCMGenerate_img2img_controlnet", + "LCMGenerate_inpaintv2", + "LCMGenerate_inpaintv3", "LCMLoader", "LCMLoader_RefInpaint", "LCMLoader_ReferenceOnly", "LCMLoader_controlnet", + "LCMLoader_controlnet_inpaint", "LCMLoader_img2img", "LCMT2IAdapter", + "LCM_IPAdapter", + "LCM_IPAdapter_inpaint", "LCM_outpaint_prep", "LoadImageNode_LCM", "SaveImage_LCM" diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 3a6fba72..cea37546 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "author": "Jordach", + "title": "comfy-consistency-vae", + "reference": "https://github.com/Jordach/comfy-consistency-vae", + "files": [ + "https://github.com/Jordach/comfy-consistency-vae" + ], + "install_type": "git-clone", + "description": "Nodes: Comfy_ConsistencyVAE" + }, { "author": "nagolinc", "title": "ComfyUI_FastVAEDecorder_SDXL", @@ -801,97 +811,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. A8R8 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.
Used to load 4-bit GPTQ Llama/2 models. You can find a lot of them over at https://huggingface.co/TheBloke

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 here." - }, - { - "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." } ] } diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 3ef06e0d..3d32db6a 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -586,6 +586,14 @@ "title_aux": "Rembg Background Removal Node for ComfyUI" } ], + "https://github.com/Jordach/comfy-consistency-vae": [ + [ + "Comfy_ConsistencyVAE" + ], + { + "title_aux": "comfy-consistency-vae" + } + ], "https://github.com/Jordach/comfy-plasma": [ [ "JDC_AutoContrast", @@ -3897,13 +3905,19 @@ "LCMGenerate", "LCMGenerate_ReferenceOnly", "LCMGenerate_img2img", + "LCMGenerate_img2img_IPAdapter", "LCMGenerate_img2img_controlnet", + "LCMGenerate_inpaintv2", + "LCMGenerate_inpaintv3", "LCMLoader", "LCMLoader_RefInpaint", "LCMLoader_ReferenceOnly", "LCMLoader_controlnet", + "LCMLoader_controlnet_inpaint", "LCMLoader_img2img", "LCMT2IAdapter", + "LCM_IPAdapter", + "LCM_IPAdapter_inpaint", "LCM_outpaint_prep", "LoadImageNode_LCM", "SaveImage_LCM" From 5dd9848017b4523cd1011a3fa9ed7a779c88da4c Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Tue, 7 Nov 2023 07:55:19 +0900 Subject: [PATCH 57/59] update DB --- custom-node-list.json | 10 ---------- node_db/dev/custom-node-list.json | 10 ++++++++++ node_db/new/custom-node-list.json | 10 ---------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/custom-node-list.json b/custom-node-list.json index 98080067..280dca44 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -600,16 +600,6 @@ "install_type": "git-clone", "description": "Nodes: Plasma Noise, Random Noise, Greyscale Noise, Pink Noise, Brown Noise, Plasma KSampler" }, - { - "author": "Jordach", - "title": "comfy-consistency-vae", - "reference": "https://github.com/Jordach/comfy-consistency-vae", - "files": [ - "https://github.com/Jordach/comfy-consistency-vae" - ], - "install_type": "git-clone", - "description": "Nodes: Comfy_ConsistencyVAE" - }, { "author": "bvhari", "title": "ImageProcessing", diff --git a/node_db/dev/custom-node-list.json b/node_db/dev/custom-node-list.json index 88a3eeae..22a6a578 100644 --- a/node_db/dev/custom-node-list.json +++ b/node_db/dev/custom-node-list.json @@ -1,5 +1,15 @@ { "custom_nodes": [ + { + "author": "Jordach", + "title": "comfy-consistency-vae", + "reference": "https://github.com/Jordach/comfy-consistency-vae", + "files": [ + "https://github.com/Jordach/comfy-consistency-vae" + ], + "install_type": "git-clone", + "description": "Nodes: Comfy_ConsistencyVAE" + }, { "author": "gameltb", "title": "ComfyUI_stable_fast", diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index cea37546..e7fac09f 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,15 +1,5 @@ { "custom_nodes": [ - { - "author": "Jordach", - "title": "comfy-consistency-vae", - "reference": "https://github.com/Jordach/comfy-consistency-vae", - "files": [ - "https://github.com/Jordach/comfy-consistency-vae" - ], - "install_type": "git-clone", - "description": "Nodes: Comfy_ConsistencyVAE" - }, { "author": "nagolinc", "title": "ComfyUI_FastVAEDecorder_SDXL", From 6d0d838629da1760825545b2574d85308b96057d Mon Sep 17 00:00:00 2001 From: "dr.lt.data" Date: Tue, 7 Nov 2023 15:22:29 +0900 Subject: [PATCH 58/59] fix: popup message zindex --- __init__.py | 2 +- js/comfyui-manager.js | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/__init__.py b/__init__.py index 498f9d94..df0f5907 100644 --- a/__init__.py +++ b/__init__.py @@ -11,7 +11,7 @@ import subprocess # don't remove this from tqdm.auto import tqdm import concurrent -version = "V0.40" +version = "V0.40.1" print(f"### Loading: ComfyUI-Manager ({version})") diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index 239de383..4a227d21 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -84,24 +84,24 @@ async function updateComfyUI() { if(response.status == 400) { app.ui.dialog.show('Failed to update ComfyUI.'); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; return false; } if(response.status == 201) { app.ui.dialog.show('ComfyUI has been successfully updated.'); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; } else { app.ui.dialog.show('ComfyUI is already up to date with the latest version.'); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; } return true; } catch(exception) { app.ui.dialog.show(`Failed to update ComfyUI / ${exception}`); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; return false; } finally { @@ -126,25 +126,25 @@ async function fetchUpdates(update_check_checkbox) { if(response.status != 200 && response.status != 201) { app.ui.dialog.show('Failed to fetch updates.'); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; return false; } if(response.status == 201) { app.ui.dialog.show('There is an updated extension available.'); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; update_check_checkbox.checked = false; } else { app.ui.dialog.show('All extensions are already up-to-date with the latest versions.'); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; } return true; } catch(exception) { app.ui.dialog.show(`Failed to update custom nodes / ${exception}`); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; return false; } finally { @@ -171,23 +171,23 @@ async function updateAll(update_check_checkbox) { if(response1.status != 200 && response2.status != 201) { app.ui.dialog.show('Failed to update ComfyUI or several extensions.

See terminal log.
'); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; return false; } if(response1.status == 201 || response2.status == 201) { app.ui.dialog.show('ComfyUI and all extensions have been updated to the latest version.'); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; } else { app.ui.dialog.show('ComfyUI and all extensions are already up-to-date with the latest versions.'); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; } return true; } catch(exception) { app.ui.dialog.show(`Failed to update ComfyUI or several extensions / ${exception}`); - app.ui.dialog.element.style.zIndex = 9999; + app.ui.dialog.element.style.zIndex = 10010; return false; } finally { From 11b2e8ac45312e69bb090c841f19d36de90b039e Mon Sep 17 00:00:00 2001 From: "dr.lt.data" Date: Tue, 7 Nov 2023 15:32:46 +0900 Subject: [PATCH 59/59] feat: ssl bypass --- __init__.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/__init__.py b/__init__.py index df0f5907..40943d80 100644 --- a/__init__.py +++ b/__init__.py @@ -10,8 +10,9 @@ import locale import subprocess # don't remove this from tqdm.auto import tqdm import concurrent +import ssl -version = "V0.40.1" +version = "V0.41" print(f"### Loading: ComfyUI-Manager ({version})") @@ -111,7 +112,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) @@ -141,7 +143,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: @@ -150,7 +153,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 } @@ -1312,13 +1316,8 @@ 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) - - try: - download_url(json_data['url'], model_dir) - except: - fallback_url = json_data['url'].replace('https', 'http') - download_url(fallback_url, model_dir) - + 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) @@ -1383,6 +1382,10 @@ async def channel_url_list(request): return web.Response(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']