WARNING: Installing the current version is causing an issue where ComfyUI fails to start.
"
+ },
+ {
+ "author": "Fannovel16",
+ "title": "ControlNet Preprocessors",
+ "reference": "https://github.com/Fannovel16/comfy_controlnet_preprocessors",
+ "files": [
+ "https://github.com/Fannovel16/comfy_controlnet_preprocessors"
+ ],
+ "install_type": "git-clone",
+ "description": "ControlNet Preprocessors. (To use this extension, you need to download the required model file from Install Models)
NOTE: Please uninstall this custom node and instead install 'ComfyUI's ControlNet Auxiliary Preprocessors' from the default channel. To use nodes belonging to controlnet v1 such as Canny_Edge_Preprocessor, MIDAS_Depth_Map_Preprocessor, Uniformer_SemSegPreprocessor, etc., you need to copy the config.yaml.example file to config.yaml and change skip_v1: True to skip_v1: False.
"
+ },
+ {
+ "author": "comfyanonymous",
+ "title": "ComfyUI_experiments/sampler_tonemap",
+ "reference": "https://github.com/comfyanonymous/ComfyUI_experiments",
+ "files": [
+ "https://github.com/comfyanonymous/ComfyUI_experiments/raw/master/sampler_tonemap.py"
+ ],
+ "install_type": "copy",
+ "description": "ModelSamplerTonemapNoiseTest a node that makes the sampler use a simple tonemapping algorithm to tonemap the noise. It will let you use higher CFG without breaking the image. To using higher CFG lower the multiplier value. Similar to Dynamic Thresholding extension of A1111. "
+ },
+ {
+ "author": "comfyanonymous",
+ "title": "ComfyUI_experiments/sampler_rescalecfg",
+ "reference": "https://github.com/comfyanonymous/ComfyUI_experiments",
+ "files": [
+ "https://github.com/comfyanonymous/ComfyUI_experiments/raw/master/sampler_rescalecfg.py"
+ ],
+ "install_type": "copy",
+ "description": "RescaleClassifierFreeGuidance improves the problem of images being degraded by high CFG.To using higher CFG lower the multiplier value. Similar to Dynamic Thresholding extension of A1111. (reference paper)
It is recommended to use the integrated custom nodes in the default channel for update support rather than installing individual nodes.
"
+ },
+ {
+ "author": "comfyanonymous",
+ "title": "ComfyUI_experiments/advanced_model_merging",
+ "reference": "https://github.com/comfyanonymous/ComfyUI_experiments",
+ "files": [
+ "https://github.com/comfyanonymous/ComfyUI_experiments/raw/master/advanced_model_merging.py"
+ ],
+ "install_type": "copy",
+ "description": "This provides a detailed model merge feature based on block weight. ModelMergeBlock, in vanilla ComfyUI, allows for adjusting the ratios of input/middle/output layers, but this node provides ratio adjustments for all blocks within each layer.
It is recommended to use the integrated custom nodes in the default channel for update support rather than installing individual nodes.
"
+ },
+ {
+ "author": "comfyanonymous",
+ "title": "ComfyUI_experiments/sdxl_model_merging",
+ "reference": "https://github.com/comfyanonymous/ComfyUI_experiments",
+ "files": [
+ "https://github.com/comfyanonymous/ComfyUI_experiments/raw/master/sdxl_model_merging.py"
+ ],
+ "install_type": "copy",
+ "description": "These nodes provide the capability to merge SDXL base models.
It is recommended to use the integrated custom nodes in the default channel for update support rather than installing individual nodes.
It is recommended to use the integrated custom nodes in the default channel for update support rather than installing individual nodes.
"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/legacy/extension-node-map.json b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/legacy/extension-node-map.json
new file mode 100644
index 0000000000000000000000000000000000000000..9e26dfeeb6e641a33dae4961196235bdb965b21b
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/legacy/extension-node-map.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/legacy/model-list.json b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/legacy/model-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..8e3e1dc4858a08aa46190aa53ba320d565206cf4
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/legacy/model-list.json
@@ -0,0 +1,3 @@
+{
+ "models": []
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/alter-list.json b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/alter-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..072c3bb5e8bd05b6f14f6df25386dc1e1010a137
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/alter-list.json
@@ -0,0 +1,4 @@
+{
+ "items": [
+ ]
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/custom-node-list.json b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/custom-node-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..bbe212a5cd2315c445ce586165395d3ed48974a1
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/custom-node-list.json
@@ -0,0 +1,700 @@
+{
+ "custom_nodes": [
+ {
+ "author": "#NOTICE_1.13",
+ "title": "NOTICE: This channel is not the default channel.",
+ "reference": "https://github.com/ltdrdata/ComfyUI-Manager",
+ "files": [],
+ "install_type": "git-clone",
+ "description": "If you see this message, your ComfyUI-Manager is outdated.\nRecent channel provides only the list of the latest nodes. If you want to find the complete node list, please go to the Default channel."
+ },
+
+
+
+ {
+ "author": "Ryuukeisyou",
+ "title": "comfyui_face_parsing",
+ "reference": "https://github.com/Ryuukeisyou/comfyui_face_parsing",
+ "files": [
+ "https://github.com/Ryuukeisyou/comfyui_face_parsing"
+ ],
+ "install_type": "git-clone",
+ "description": "This is a set of custom nodes for ComfyUI. The nodes utilize the [a/face parsing model](https://huggingface.co/jonathandinu/face-parsing) to provide detailed segmantation of face. To improve face segmantation accuracy, [a/yolov8 face model](https://huggingface.co/Bingsu/adetailer/) is used to first extract face from an image. There are also auxiliary nodes for image and mask processing. A guided filter is also provided for skin smoothing."
+ },
+ {
+ "author": "florestefano1975",
+ "title": "comfyui-prompt-composer",
+ "reference": "https://github.com/florestefano1975/comfyui-prompt-composer",
+ "files": [
+ "https://github.com/florestefano1975/comfyui-prompt-composer"
+ ],
+ "install_type": "git-clone",
+ "description": "A suite of tools for prompt management. Combining nodes helps the user sequence strings for prompts, also creating logical groupings if necessary. Individual nodes can be chained together in any order."
+ },
+ {
+ "author": "ai-liam",
+ "title": "LiamUtil",
+ "reference": "https://github.com/ai-liam/comfyui_liam_util",
+ "files": [
+ "https://github.com/ai-liam/comfyui_liam_util"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes: LiamLoadImage. This node provides the capability to load images from a URL."
+ },
+ {
+ "author": "jesenzhang",
+ "title": "ComfyUI_StreamDiffusion",
+ "reference": "https://github.com/jesenzhang/ComfyUI_StreamDiffusion",
+ "files": [
+ "https://github.com/jesenzhang/ComfyUI_StreamDiffusion"
+ ],
+ "install_type": "git-clone",
+ "description": "This is a simple implementation StreamDiffusion(A Pipeline-Level Solution for Real-Time Interactive Generation) for ComfyUI"
+ },
+ {
+ "author": "an90ray",
+ "title": "ComfyUI-DareMerge",
+ "reference": "https://github.com/an90ray/ComfyUI_RErouter_CustomNodes",
+ "files": [
+ "https://github.com/an90ray/ComfyUI_RErouter_CustomNodes"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes: RErouter, String (RE), Int (RE)"
+ },
+ {
+ "author": "Crystian",
+ "title": "Crystools",
+ "reference": "https://github.com/crystian/ComfyUI-Crystools",
+ "files": [
+ "https://github.com/crystian/ComfyUI-Crystools"
+ ],
+ "install_type": "git-clone",
+ "description": "You can see the metadata and compare between two images, compare between two JSONs, show any value to console/display, pipes, and more!\nThis provides a better nodes to load images, previews, etc, and see \"hidden\" data, but without load a new workflow."
+ },
+ {
+ "author": "54rt1n",
+ "title": "ComfyUI-DareMerge",
+ "reference": "https://github.com/54rt1n/ComfyUI-DareMerge",
+ "files": [
+ "https://github.com/54rt1n/ComfyUI-DareMerge"
+ ],
+ "install_type": "git-clone",
+ "description": "Merge two checkpoint models by dare ties [a/(https://github.com/yule-BUAA/MergeLM)](https://github.com/yule-BUAA/MergeLM), sort of."
+ },
+ {
+ "author": "Kangkang625",
+ "title": "ComfyUI-Paint-by-Example",
+ "reference": "https://github.com/Kangkang625/ComfyUI-paint-by-example",
+ "pip": ["diffusers"],
+ "files": [
+ "https://github.com/Kangkang625/ComfyUI-paint-by-example"
+ ],
+ "install_type": "git-clone",
+ "description": "This repo is a simple implementation of [a/Paint-by-Example](https://github.com/Fantasy-Studio/Paint-by-Example) based on its [a/huggingface pipeline](https://huggingface.co/Fantasy-Studio/Paint-by-Example)."
+ },
+ {
+ "author": "pkpk",
+ "title": "ComfyUI-SaveAVIF",
+ "reference": "https://github.com/pkpkTech/ComfyUI-SaveAVIF",
+ "files": [
+ "https://github.com/pkpkTech/ComfyUI-SaveAVIF"
+ ],
+ "install_type": "git-clone",
+ "description": "A custom node on ComfyUI that saves images in AVIF format. Workflow can be loaded from images saved at this node."
+ },
+ {
+ "author": "styler00dollar",
+ "title": "ComfyUI-deepcache",
+ "reference": "https://github.com/styler00dollar/ComfyUI-deepcache",
+ "files": [
+ "https://github.com/styler00dollar/ComfyUI-deepcache"
+ ],
+ "install_type": "git-clone",
+ "description": "This extension provides nodes for [a/DeepCache: Accelerating Diffusion Models for Free](https://arxiv.org/abs/2312.00858)\nNOTE:Original code can be found [a/here](https://gist.github.com/laksjdjf/435c512bc19636e9c9af4ee7bea9eb86). Full credit to laksjdjf for sharing the code. "
+ },
+ {
+ "author": "edenartlab",
+ "title": "eden_comfy_pipelines",
+ "reference": "https://github.com/edenartlab/eden_comfy_pipelines",
+ "files": [
+ "https://github.com/edenartlab/eden_comfy_pipelines"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes:CLIP Interrogator."
+ },
+ {
+ "author": "Limitex",
+ "title": "ComfyUI-Diffusers",
+ "reference": "https://github.com/Limitex/ComfyUI-Diffusers",
+ "files": [
+ "https://github.com/Limitex/ComfyUI-Diffusers"
+ ],
+ "install_type": "git-clone",
+ "description": "This extension enables the use of the diffuser pipeline in ComfyUI."
+ },
+ {
+ "author": "Limitex",
+ "title": "ComfyUI-Calculation",
+ "reference": "https://github.com/Limitex/ComfyUI-Calculation",
+ "files": [
+ "https://github.com/Limitex/ComfyUI-Calculation"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes: Center Calculation. Improved Numerical Calculation for ComfyUI"
+ },
+ {
+ "author": "Comfyui_GPT_Story",
+ "title": "junglehu",
+ "reference": "https://github.com/junglehu/Comfyui_Story_LLmA",
+ "files": [
+ "https://github.com/junglehu/Comfyui_Story_LLmA"
+ ],
+ "install_type": "git-clone",
+ "description": "GPT+Comfyui Generate coherent pictures"
+ },
+ {
+ "author": "HarroweD and quadmoon",
+ "title": "Harronode",
+ "reference": "https://github.com/NotHarroweD/Harronode",
+ "nodename_pattern": "Harronode",
+ "files": [
+ "https://github.com/NotHarroweD/Harronode.git"
+ ],
+ "install_type": "git-clone",
+ "description": "Harronode is a custom node designed to build prompts easily for use with the Harrlogos SDXL LoRA. This Node simplifies the process of crafting prompts and makes all built in activation terms available at your fingertips."
+ },
+ {
+ "author": "styler00dollar",
+ "title": "ComfyUI-sudo-latent-upscale",
+ "reference": "https://github.com/styler00dollar/ComfyUI-sudo-latent-upscale",
+ "files": [
+ "https://github.com/styler00dollar/ComfyUI-sudo-latent-upscale"
+ ],
+ "install_type": "git-clone",
+ "description": "Directly upscaling inside the latent space. Model was trained for SD1.5 and drawn content. Might add new architectures or update models at some point. This took heavy inspriration from [city96/SD-Latent-Upscaler](https://github.com/city96/SD-Latent-Upscaler) and [Ttl/ComfyUi_NNLatentUpscale](https://github.com/Ttl/ComfyUi_NNLatentUpscale). "
+ },
+ {
+ "author": "thecooltechguy",
+ "title": "ComfyUI-ComfyRun",
+ "reference": "https://github.com/thecooltechguy/ComfyUI-ComfyRun",
+ "files": [
+ "https://github.com/thecooltechguy/ComfyUI-ComfyRun"
+ ],
+ "install_type": "git-clone",
+ "description": "The easiest way to run & share any ComfyUI workflow [a/https://comfyrun.com](https://comfyrun.com)"
+ },
+ {
+ "author": "ceruleandeep",
+ "title": "ComfyUI LLaVA Captioner",
+ "reference": "https://github.com/ceruleandeep/ComfyUI-LLaVA-Captioner",
+ "files": [
+ "https://github.com/ceruleandeep/ComfyUI-LLaVA-Captioner"
+ ],
+ "install_type": "git-clone",
+ "description": "A ComfyUI extension for chatting with your images. Runs on your own system, no external services used, no filter. Uses the [a/LLaVA multimodal LLM](https://llava-vl.github.io/) so you can give instructions or ask questions in natural language. It's maybe as smart as GPT3.5, and it can see."
+ },
+ {
+ "author": "jitcoder",
+ "title": "LoraInfo",
+ "reference": "https://github.com/jitcoder/lora-info",
+ "files": [
+ "https://github.com/jitcoder/lora-info"
+ ],
+ "install_type": "git-clone",
+ "description": "Shows Lora information from CivitAI and outputs trigger words and example prompt"
+ },
+ {
+ "author": "ttulttul",
+ "title": "ComfyUI Iterative Mixing Nodes",
+ "reference": "https://github.com/ttulttul/ComfyUI-Iterative-Mixer",
+ "files": [
+ "https://github.com/ttulttul/ComfyUI-Iterative-Mixer"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes: Iterative Mixing KSampler, Batch Unsampler, Iterative Mixing KSampler Advanced"
+ },
+ {
+ "author": "OpenArt-AI",
+ "title": "ComfyUI Assistant",
+ "reference": "https://github.com/OpenArt-AI/ComfyUI-Assistant",
+ "files": [
+ "https://github.com/OpenArt-AI/ComfyUI-Assistant"
+ ],
+ "install_type": "git-clone",
+ "description": "ComfyUI Assistant is your one stop plugin for everything you need to get started with comfy-ui. Now it provides useful courses, tutorials, and basic templates."
+ },
+ {
+ "author": "shockz0rz",
+ "title": "comfy-easy-grids",
+ "reference": "https://github.com/shockz0rz/comfy-easy-grids",
+ "files": [
+ "https://github.com/shockz0rz/comfy-easy-grids"
+ ],
+ "install_type": "git-clone",
+ "description": "A set of custom nodes for creating image grids, sequences, and batches in ComfyUI."
+ },
+ {
+ "author": "CosmicLaca",
+ "title": "Primere nodes for ComfyUI",
+ "reference": "https://github.com/CosmicLaca/ComfyUI_Primere_Nodes",
+ "files": [
+ "https://github.com/CosmicLaca/ComfyUI_Primere_Nodes"
+ ],
+ "install_type": "git-clone",
+ "description": "This extension provides various utility nodes. Inputs(prompt, styles, dynamic, merger, ...), Outputs(style pile), Dashboard(selectors, loader, switch, ...), Networks(LORA, Embedding, Hypernetwork), Visuals(visual selectors, )"
+ },
+ {
+ "author": "ZHO-ZHO-ZHO",
+ "title": "ComfyUI-Gemini",
+ "reference": "https://github.com/ZHO-ZHO-ZHO/ComfyUI-Gemini",
+ "files": [
+ "https://github.com/ZHO-ZHO-ZHO/ComfyUI-Gemini"
+ ],
+ "install_type": "git-clone",
+ "description": "Using Gemini-pro & Gemini-pro-vision in ComfyUI."
+ },
+ {
+ "author": "ZHO-ZHO-ZHO",
+ "title": "comfyui-portrait-master-zh-cn",
+ "reference": "https://github.com/ZHO-ZHO-ZHO/comfyui-portrait-master-zh-cn",
+ "files": [
+ "https://github.com/ZHO-ZHO-ZHO/comfyui-portrait-master-zh-cn"
+ ],
+ "install_type": "git-clone",
+ "description": "ComfyUI Portrait Master 简体中文版."
+ },
+ {
+ "author": "Continue7777",
+ "title": "comfyui-easyapi-nodes",
+ "reference": "https://github.com/Continue7777/comfyui-yoy",
+ "files": [
+ "https://github.com/Continue7777/comfyui-yoy"
+ ],
+ "install_type": "git-clone",
+ "description": "This plugin gives you the ability to generate a real Product rendering, commonly referred to as the mockup.The most important is that we can provide it for you,cause we have a complete supply chain.You can buy products with your designs or sell them. We can provide you dropshipping services."
+ },
+ {
+ "author": "RenderRift",
+ "title": "ComfyUI-RenderRiftNodes",
+ "reference": "https://github.com/RenderRift/ComfyUI-RenderRiftNodes",
+ "files": [
+ "https://github.com/RenderRift/ComfyUI-RenderRiftNodes"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes:RR_Date_Folder_Format, RR_Image_Metadata_Overlay, RR_VideoPathMetaExtraction, RR_DisplayMetaOptions. This extension provides nodes designed to enhance the Animatediff workflow."
+ },
+ {
+ "author": "Haoming02",
+ "title": "ComfyUI Floodgate",
+ "reference": "https://github.com/Haoming02/comfyui-floodgate",
+ "files": [
+ "https://github.com/Haoming02/comfyui-floodgate"
+ ],
+ "install_type": "git-clone",
+ "description": "This is an Extension for ComfyUI, which allows you to control the logic flow with just one click!"
+ },
+ {
+ "author": "Trung0246",
+ "title": "ComfyUI-0246",
+ "reference": "https://github.com/Trung0246/ComfyUI-0246",
+ "files": [
+ "https://github.com/Trung0246/ComfyUI-0246"
+ ],
+ "install_type": "git-clone",
+ "description": "Random nodes for ComfyUI I made to solve my struggle with ComfyUI (ex: pipe, process). Have varying quality."
+ },
+ {
+ "author": "violet-chen",
+ "title": "comfyui-psd2png",
+ "reference": "https://github.com/violet-chen/comfyui-psd2png",
+ "files": [
+ "https://github.com/violet-chen/comfyui-psd2png"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes: Psd2Png."
+ },
+ {
+ "author": "IDGallagher",
+ "title": "IG Interpolation Nodes",
+ "reference": "https://github.com/IDGallagher/ComfyUI-IG-Nodes",
+ "files": [
+ "https://github.com/IDGallagher/ComfyUI-IG-Nodes"
+ ],
+ "install_type": "git-clone",
+ "description": "Custom nodes to aid in the exploration of Latent Space"
+ },
+ {
+ "author": "rcfcu2000",
+ "title": "zhihuige-nodes-comfyui",
+ "reference": "https://github.com/rcfcu2000/zhihuige-nodes-comfyui",
+ "files": [
+ "https://github.com/rcfcu2000/zhihuige-nodes-comfyui"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes: Combine ZHGMasks, Cover ZHGMasks, ZHG FaceIndex, ZHG SaveImage, ZHG SmoothEdge, ZHG GetMaskArea, ..."
+ },
+ {
+ "author": "rcsaquino",
+ "title": "rcsaquino/comfyui-custom-nodes",
+ "reference": "https://github.com/rcsaquino/comfyui-custom-nodes",
+ "files": [
+ "https://github.com/rcsaquino/comfyui-custom-nodes"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes: VAE Processor, VAE Loader, Background Remover"
+ },
+ {
+ "author": "mozman",
+ "title": "ComfyUI_mozman_nodes",
+ "reference": "https://github.com/mozman/ComfyUI_mozman_nodes",
+ "files": [
+ "https://github.com/mozman/ComfyUI_mozman_nodes"
+ ],
+ "install_type": "git-clone",
+ "description": "This extension provides styler nodes for SDXL.\n\nNOTE: Due to the dynamic nature of node name definitions, ComfyUI-Manager cannot recognize the node list from this extension. The Missing nodes and Badge features are not available for this extension."
+ },
+ {
+ "author": "meap158",
+ "title": "ComfyUI-Background-Replacement",
+ "reference": "https://github.com/meap158/ComfyUI-Background-Replacement",
+ "files": [
+ "https://github.com/meap158/ComfyUI-Background-Replacement"
+ ],
+ "install_type": "git-clone",
+ "description": "Instantly replace your image's background."
+ },
+ {
+ "author": "florestefano1975",
+ "title": "comfyui-portrait-master",
+ "reference": "https://github.com/florestefano1975/comfyui-portrait-master",
+ "files": [
+ "https://github.com/florestefano1975/comfyui-portrait-master"
+ ],
+ "install_type": "git-clone",
+ "description": "ComfyUI Portrait Master. A node designed to help AI image creators to generate prompts for human portraits."
+ },
+ {
+ "author": "deroberon",
+ "title": "StableZero123-comfyui",
+ "reference": "https://github.com/deroberon/StableZero123-comfyui",
+ "files": [
+ "https://github.com/deroberon/StableZero123-comfyui"
+ ],
+ "install_type": "git-clone",
+ "description": "StableZero123 is a node wrapper that uses the model and technique provided [here](https://github.com/SUDO-AI-3D/zero123plus/). It uses the Zero123plus model to generate 3D views using just one image."
+ },
+ {
+ "author": "dmarx",
+ "title": "ComfyUI-Keyframed",
+ "reference": "https://github.com/dmarx/ComfyUI-Keyframed",
+ "files": [
+ "https://github.com/dmarx/ComfyUI-Keyframed"
+ ],
+ "install_type": "git-clone",
+ "description": "ComfyUI nodes to facilitate parameter/prompt keyframing using comfyui nodes for defining and manipulating parameter curves. Essentially provides a ComfyUI interface to the [a/keyframed](https://github.com/dmarx/keyframed) library."
+ },
+ {
+ "author": "TripleHeadedMonkey",
+ "title": "ComfyUI_MileHighStyler",
+ "reference": "https://github.com/TripleHeadedMonkey/ComfyUI_MileHighStyler",
+ "files": [
+ "https://github.com/TripleHeadedMonkey/ComfyUI_MileHighStyler"
+ ],
+ "install_type": "git-clone",
+ "description": "This extension provides various SDXL Prompt Stylers. See: [a/youtube](https://youtu.be/WBHI-2uww7o?si=dijvDaUI4nmx4VkF)"
+ },
+ {
+ "author": "Extraltodeus",
+ "title": "sigmas_tools_and_the_golden_scheduler",
+ "reference": "https://github.com/Extraltodeus/sigmas_tools_and_the_golden_scheduler",
+ "files": [
+ "https://github.com/Extraltodeus/sigmas_tools_and_the_golden_scheduler"
+ ],
+ "install_type": "git-clone",
+ "description": "A few nodes to mix sigmas and a custom scheduler that uses phi, then one using eval() to be able to schedule with custom formulas."
+ },
+ {
+ "author": "BennyKok",
+ "title": "ComfyUI Deploy",
+ "reference": "https://github.com/BennyKok/comfyui-deploy",
+ "files": [
+ "https://github.com/BennyKok/comfyui-deploy"
+ ],
+ "install_type": "git-clone",
+ "description": "Open source comfyui deployment platform, a vercel for generative workflow infra."
+ },
+ {
+ "author": "Rui",
+ "title": "RUI-Nodes",
+ "reference": "https://github.com/rui40000/RUI-Nodes",
+ "files": [
+ "https://github.com/rui40000/RUI-Nodes"
+ ],
+ "install_type": "git-clone",
+ "description": "Rui's workflow-specific custom node, written using GPT."
+ },
+ {
+ "author": "SpaceKendo",
+ "title": "Text to video for Stable Video Diffusion in ComfyUI",
+ "reference": "https://github.com/SpaceKendo/ComfyUI-svd_txt2vid",
+ "files": [
+ "https://github.com/SpaceKendo/ComfyUI-svd_txt2vid"
+ ],
+ "install_type": "git-clone",
+ "description": "This is node replaces the init_image conditioning for the [a/Stable Video Diffusion](https://github.com/Stability-AI/generative-models) image to video model with text embeds, together with a conditioning frame. The conditioning frame is a set of latents."
+ },
+ {
+ "author": "NimaNzrii",
+ "title": "comfyui-photoshop",
+ "reference": "https://github.com/NimaNzrii/comfyui-photoshop",
+ "files": [
+ "https://github.com/NimaNzrii/comfyui-photoshop"
+ ],
+ "install_type": "git-clone",
+ "description": "Photoshop node inside of ComfyUi, send and get data from Photoshop"
+ },
+ {
+ "author": "NimaNzrii",
+ "title": "comfyui-popup_preview",
+ "reference": "https://github.com/NimaNzrii/comfyui-popup_preview",
+ "files": [
+ "https://github.com/NimaNzrii/comfyui-popup_preview"
+ ],
+ "install_type": "git-clone",
+ "description": "popup preview for comfyui"
+ },
+
+
+ {
+ "author": "AI2lab",
+ "title": "comfyUI-tool-2lab",
+ "reference": "https://github.com/AI2lab/comfyUI-tool-2lab",
+ "files": [
+ "https://github.com/AI2lab/comfyUI-tool-2lab"
+ ],
+ "install_type": "git-clone",
+ "description": "Integrate non-painting capabilities into comfyUI, including data, algorithms, video processing, large models, etc., to facilitate the construction of more powerful workflows."
+ },
+ {
+ "author": "LZC",
+ "title": "Hayo comfyui nodes",
+ "reference": "https://github.com/1shadow1/hayo_comfyui_nodes",
+ "files": [
+ "https://github.com/1shadow1/hayo_comfyui_nodes/raw/main/LZCNodes.py"
+ ],
+ "install_type": "copy",
+ "description": "Nodes:tensor_trans_pil, Make Transparent mask, MergeImages, words_generatee, load_PIL image"
+ },
+ {
+ "author": "MNeMoNiCuZ",
+ "title": "ComfyUI-mnemic-nodes",
+ "reference": "https://github.com/MNeMoNiCuZ/ComfyUI-mnemic-nodes",
+ "files": [
+ "https://github.com/MNeMoNiCuZ/ComfyUI-mnemic-nodes"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes:Save Text File"
+ },
+ {
+ "author": "vienteck",
+ "title": "ComfyUI-Chat-GPT-Integration",
+ "reference": "https://github.com/vienteck/ComfyUI-Chat-GPT-Integration",
+ "files": [
+ "https://github.com/vienteck/ComfyUI-Chat-GPT-Integration"
+ ],
+ "install_type": "git-clone",
+
+ "description": "This extension is a reimagined version based on the [a/ComfyUI-QualityOfLifeSuit_Omar92](https://github.com/omar92/ComfyUI-QualityOfLifeSuit_Omar92) extension, and it supports integration with ChatGPT through the new OpenAI API.\nNOTE: See detailed installation instructions on the [a/repository](https://github.com/vienteck/ComfyUI-Chat-GPT-Integration)."
+ },
+ {
+ "author": "Haoming02",
+ "title": "ComfyUI Tab Handler",
+ "reference": "https://github.com/Haoming02/comfyui-tab-handler",
+ "files": [
+ "https://github.com/Haoming02/comfyui-tab-handler"
+ ],
+ "install_type": "git-clone",
+ "description": "This is an Extension for ComfyUI, which moves the menu to the specified corner on startup."
+ },
+ {
+ "author": "glibsonoran",
+ "title": "Plush-for-ComfyUI",
+ "reference": "https://github.com/glibsonoran/Plush-for-ComfyUI",
+ "files": [
+ "https://github.com/glibsonoran/Plush-for-ComfyUI"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes: Style Prompt, OAI Dall_e Image. Plush contains two OpenAI enabled nodes: Style Prompt: Takes your prompt and the art style you specify and generates a prompt from ChatGPT3 or 4 that Stable Diffusion can use to generate an image in that style. OAI Dall_e 3: Takes your prompt and parameters and produces a Dall_e3 image in ComfyUI."
+ },
+ {
+ "author": "Aegis72",
+ "title": "AegisFlow Utility Nodes",
+ "reference": "https://github.com/aegis72/aegisflow_utility_nodes",
+ "files": [
+ "https://github.com/aegis72/aegisflow_utility_nodes"
+ ],
+ "install_type": "git-clone",
+ "description": "These nodes will be placed in comfyui/custom_nodes/aegisflow and contains the image passer (accepts an image as either wired or wirelessly, input and passes it through. Latent passer does the same for latents, and the Preprocessor chooser allows a passthrough image and 10 controlnets to be passed in AegisFlow Shima. The inputs on the Preprocessor chooser should not be renamed if you intend to accept image inputs wirelessly through UE nodes. It can be done, but the send node input regex for each controlnet preprocessor column must also be changed."
+ },
+ {
+ "author": "concarne000",
+ "title": "ConCarneNode",
+ "reference": "https://github.com/concarne000/ConCarneNode",
+ "files": [
+ "https://github.com/concarne000/ConCarneNode"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes:Bing Image Grabber node for ComfyUI."
+ },
+ {
+ "author": "Haoming02",
+ "title": "ComfyUI Menu Anchor",
+ "reference": "https://github.com/Haoming02/comfyui-menu-anchor",
+ "files": [
+ "https://github.com/Haoming02/comfyui-menu-anchor"
+ ],
+ "install_type": "git-clone",
+ "description": "This is an Extension for ComfyUI, which moves the menu to the specified corner on startup."
+ },
+ {
+ "author": "glifxyz",
+ "title": "ComfyUI-GlifNodes",
+ "reference": "https://github.com/glifxyz/ComfyUI-GlifNodes",
+ "files": [
+ "https://github.com/glifxyz/ComfyUI-GlifNodes"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes:Consistency VAE Decoder."
+ },
+ {
+ "author": "kijai",
+ "title": "Marigold depth estimation in ComfyUI",
+ "reference": "https://github.com/kijai/ComfyUI-Marigold",
+ "files": [
+ "https://github.com/kijai/ComfyUI-Marigold"
+ ],
+ "install_type": "git-clone",
+ "description": "This is a wrapper node for Marigold depth estimation: [https://github.com/prs-eth/Marigold](https://github.com/kijai/ComfyUI-Marigold). Currently using the same diffusers pipeline as in the original implementation, so in addition to the custom node, you need the model in diffusers format.\nNOTE: See details in repo to install."
+ },
+ {
+ "author": "spacepxl",
+ "title": "ComfyUI-Image-Filters",
+ "reference": "https://github.com/spacepxl/ComfyUI-Image-Filters",
+ "files": [
+ "https://github.com/spacepxl/ComfyUI-Image-Filters"
+ ],
+ "install_type": "git-clone",
+ "description": "Image and matte filtering nodes for ComfyUI `image/filters/*`"
+ },
+ {
+ "author": "Haoming02",
+ "title": "ComfyUI Clear Screen",
+ "reference": "https://github.com/Haoming02/comfyui-clear-screen",
+ "files": [
+ "https://github.com/Haoming02/comfyui-clear-screen"
+ ],
+ "install_type": "git-clone",
+ "description": "This is an Extension for ComfyUI, which adds a button, CLS, to clear the console window."
+ },
+ {
+ "author": "deroberon",
+ "title": "demofusion-comfyui",
+ "reference": "https://github.com/deroberon/demofusion-comfyui",
+ "files": [
+ "https://github.com/deroberon/demofusion-comfyui"
+ ],
+ "install_type": "git-clone",
+ "description": "The Demofusion Custom Node is a wrapper that adapts the work and implementation of the [a/DemoFusion](https://ruoyidu.github.io/demofusion/demofusion.html) technique created and implemented by Ruoyi Du to the Comfyui environment."
+ },
+ {
+ "author": "Haoming02",
+ "title": "comfyui-prompt-format",
+ "reference": "https://github.com/Haoming02/comfyui-prompt-format",
+ "files": [
+ "https://github.com/Haoming02/comfyui-prompt-format"
+ ],
+ "install_type": "git-clone",
+ "description": "This is an Extension for ComfyUI, which helps formatting texts."
+ },
+ {
+ "author": "brianfitzgerald",
+ "title": "StyleAligned for ComfyUI",
+ "reference": "https://github.com/brianfitzgerald/style_aligned_comfy",
+ "files": [
+ "https://github.com/brianfitzgerald/style_aligned_comfy"
+ ],
+ "install_type": "git-clone",
+ "description": "Implementation of the [a/StyleAligned](https://style-aligned-gen.github.io/) paper for ComfyUI. This node allows you to apply a consistent style to all images in a batch; by default it will use the first image in the batch as the style reference, forcing all other images to be consistent with it."
+ },
+ {
+ "author": "MitoshiroPJ",
+ "title": "ComfyUI Slothful Attention",
+ "reference": "https://github.com/MitoshiroPJ/comfyui_slothful_attention",
+ "files": [
+ "https://github.com/MitoshiroPJ/comfyui_slothful_attention"
+ ],
+ "install_type": "git-clone",
+ "description": "This custom node allow controlling output without training. The reducing method is similar to [a/Spatial-Reduction Attention](https://paperswithcode.com/method/spatial-reduction-attention), but generating speed may not be increased on typical image sizes due to overheads. (In some cases, slightly slower)"
+ },
+ {
+ "author": "aria1th",
+ "title": "ComfyUI-LogicUtils",
+ "reference": "https://github.com/aria1th/ComfyUI-LogicUtils",
+ "files": [
+ "https://github.com/aria1th/ComfyUI-LogicUtils"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes:UniformRandomFloat..., RandomShuffleInt, YieldableIterator..., LogicGate..., Add..., MergeString, MemoryNode, ..."
+ },
+ {
+ "author": "modusCell",
+ "title": "Preset Dimensions",
+ "reference": "https://github.com/modusCell/ComfyUI-dimension-node-modusCell",
+ "files": [
+ "https://github.com/modusCell/ComfyUI-dimension-node-modusCell"
+ ],
+ "install_type": "git-clone",
+ "description": "Simple node for sharing latent image size between nodes. Preset dimensions for SD and XL."
+ },
+ {
+ "author": "mmaker",
+ "title": "Color Enhance",
+ "reference": "https://git.mmaker.moe/mmaker/sd-webui-color-enhance",
+ "files": [
+ "https://git.mmaker.moe/mmaker/sd-webui-color-enhance"
+ ],
+ "install_type": "git-clone",
+ "description": "Node: Color Enhance, Color Blend. This is the same algorithm GIMP/GEGL uses for color enhancement. The gist of this implementation is that it converts the color space to CIELCh(ab) and normalizes the chroma (or [colorfulness](https://en.wikipedia.org/wiki/Colorfulness)] component. Original source can be found in the link below."
+ },
+ {
+ "author": "bruefire",
+ "title": "ComfyUI Sequential Image Loader",
+ "reference": "https://github.com/bruefire/ComfyUI-SeqImageLoader",
+ "files": [
+ "https://github.com/bruefire/ComfyUI-SeqImageLoader"
+ ],
+ "install_type": "git-clone",
+ "description": "This is an extension node for ComfyUI that allows you to load frames from a video in bulk and perform masking and sketching on each frame through a GUI."
+ },
+ {
+ "author": "yolain",
+ "title": "ComfyUI Easy Use",
+ "reference": "https://github.com/yolain/ComfyUI-Easy-Use",
+ "files": [
+ "https://github.com/yolain/ComfyUI-Easy-Use"
+ ],
+ "install_type": "git-clone",
+ "description": "To enhance the usability of ComfyUI, optimizations and integrations have been implemented for several commonly used nodes."
+ },
+ {
+ "author": "tzwm",
+ "title": "ComfyUI Browser",
+ "reference": "https://github.com/tzwm/comfyui-browser",
+ "files": [
+ "https://github.com/tzwm/comfyui-browser"
+ ],
+ "install_type": "git-clone",
+ "description": "This is an image/video/workflow browser and manager for ComfyUI. You could add image/video/workflow to collections and load it to ComfyUI. You will be able to use your collections everywhere."
+ }
+ ]
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/extension-node-map.json b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/extension-node-map.json
new file mode 100644
index 0000000000000000000000000000000000000000..bd5d8a2c7454659da3eff6dff612a24c536b4d48
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/extension-node-map.json
@@ -0,0 +1,6416 @@
+{
+ "https://gist.github.com/alkemann/7361b8eb966f29c8238fd323409efb68/raw/f9605be0b38d38d3e3a2988f89248ff557010076/alkemann.py": [
+ [
+ "Int to Text",
+ "Save A1 Image",
+ "Seed With Text"
+ ],
+ {
+ "title_aux": "alkemann nodes"
+ }
+ ],
+ "https://git.mmaker.moe/mmaker/sd-webui-color-enhance": [
+ [
+ "MMakerColorBlend",
+ "MMakerColorEnhance"
+ ],
+ {
+ "title_aux": "Color Enhance"
+ }
+ ],
+ "https://github.com/0xbitches/ComfyUI-LCM": [
+ [
+ "LCM_Sampler",
+ "LCM_Sampler_Advanced",
+ "LCM_img2img_Sampler",
+ "LCM_img2img_Sampler_Advanced"
+ ],
+ {
+ "title_aux": "Latent Consistency Model for ComfyUI"
+ }
+ ],
+ "https://github.com/1shadow1/hayo_comfyui_nodes/raw/main/LZCNodes.py": [
+ [
+ "LoadPILImages",
+ "MergeImages",
+ "make_transparentmask",
+ "tensor_trans_pil",
+ "words_generatee"
+ ],
+ {
+ "title_aux": "Hayo comfyui nodes"
+ }
+ ],
+ "https://github.com/42lux/ComfyUI-safety-checker": [
+ [
+ "Safety Checker"
+ ],
+ {
+ "title_aux": "ComfyUI-safety-checker"
+ }
+ ],
+ "https://github.com/54rt1n/ComfyUI-DareMerge": [
+ [
+ "DareModelMerger"
+ ],
+ {
+ "title_aux": "ComfyUI-DareMerge"
+ }
+ ],
+ "https://github.com/80sVectorz/ComfyUI-Static-Primitives": [
+ [
+ "FloatStaticPrimitive",
+ "IntStaticPrimitive",
+ "StringMlStaticPrimitive",
+ "StringStaticPrimitive"
+ ],
+ {
+ "title_aux": "ComfyUI-Static-Primitives"
+ }
+ ],
+ "https://github.com/AIrjen/OneButtonPrompt": [
+ [
+ "CreatePromptVariant",
+ "OneButtonPrompt",
+ "SavePromptToFile"
+ ],
+ {
+ "title_aux": "One Button Prompt"
+ }
+ ],
+ "https://github.com/AbdullahAlfaraj/Comfy-Photoshop-SD": [
+ [
+ "APS_LatentBatch",
+ "APS_Seed",
+ "ContentMaskLatent",
+ "ControlNetScript",
+ "ControlnetUnit",
+ "GaussianLatentImage",
+ "GetConfig",
+ "LoadImageBase64",
+ "LoadImageWithMetaData",
+ "LoadLorasFromPrompt",
+ "MaskExpansion"
+ ],
+ {
+ "title_aux": "Comfy-Photoshop-SD"
+ }
+ ],
+ "https://github.com/AbyssYuan0/ComfyUI_BadgerTools": [
+ [
+ "FloatToInt-badger",
+ "FloatToString-badger",
+ "ImageNormalization-badger",
+ "ImageOverlap-badger",
+ "ImageScaleToSide-badger",
+ "IntToString-badger",
+ "StringToFizz-badger",
+ "TextListToString-badger",
+ "getImageSide-badger"
+ ],
+ {
+ "title_aux": "ComfyUI_BadgerTools"
+ }
+ ],
+ "https://github.com/Acly/comfyui-tooling-nodes": [
+ [
+ "ETN_ApplyMaskToImage",
+ "ETN_CropImage",
+ "ETN_LoadImageBase64",
+ "ETN_LoadMaskBase64",
+ "ETN_SendImageWebSocket"
+ ],
+ {
+ "title_aux": "ComfyUI Nodes for External Tooling"
+ }
+ ],
+ "https://github.com/Amorano/Jovimetrix": [
+ [],
+ {
+ "author": "amorano",
+ "description": "Webcams, GLSL shader, Media Streaming, Tick animation, Image manipulation,",
+ "nodename_pattern": " \\(jov\\)$",
+ "title": "Jovimetrix",
+ "title_aux": "Jovimetrix Composition Nodes"
+ }
+ ],
+ "https://github.com/ArtBot2023/CharacterFaceSwap": [
+ [
+ "Color Blend",
+ "Crop Face",
+ "Exclude Facial Feature",
+ "Generation Parameter Input",
+ "Generation Parameter Output",
+ "Image Full BBox",
+ "Load BiseNet",
+ "Load RetinaFace",
+ "Mask Contour",
+ "Segment Face",
+ "Uncrop Face"
+ ],
+ {
+ "title_aux": "Character Face Swap"
+ }
+ ],
+ "https://github.com/ArtVentureX/comfyui-animatediff": [
+ [
+ "AnimateDiffCombine",
+ "AnimateDiffLoraLoader",
+ "AnimateDiffModuleLoader",
+ "AnimateDiffSampler",
+ "AnimateDiffSlidingWindowOptions",
+ "ImageSizeAndBatchSize",
+ "LoadVideo"
+ ],
+ {
+ "title_aux": "AnimateDiff"
+ }
+ ],
+ "https://github.com/AustinMroz/ComfyUI-SpliceTools": [
+ [
+ "LogSigmas",
+ "SpliceDenoised",
+ "SpliceLatents",
+ "TemporalSplice"
+ ],
+ {
+ "title_aux": "SpliceTools"
+ }
+ ],
+ "https://github.com/BadCafeCode/masquerade-nodes-comfyui": [
+ [
+ "Blur",
+ "Change Channel Count",
+ "Combine Masks",
+ "Constant Mask",
+ "Convert Color Space",
+ "Create QR Code",
+ "Create Rect Mask",
+ "Cut By Mask",
+ "Get Image Size",
+ "Image To Mask",
+ "Make Image Batch",
+ "Mask By Text",
+ "Mask Morphology",
+ "Mask To Region",
+ "MasqueradeIncrementer",
+ "Mix Color By Mask",
+ "Mix Images By Mask",
+ "Paste By Mask",
+ "Prune By Mask",
+ "Separate Mask Components",
+ "Unary Image Op",
+ "Unary Mask Op"
+ ],
+ {
+ "title_aux": "Masquerade Nodes"
+ }
+ ],
+ "https://github.com/Beinsezii/bsz-cui-extras": [
+ [
+ "BSZAbsoluteHires",
+ "BSZAspectHires",
+ "BSZColoredLatentImageXL",
+ "BSZCombinedHires",
+ "BSZHueChromaXL",
+ "BSZInjectionKSampler",
+ "BSZLatentDebug",
+ "BSZLatentFill",
+ "BSZLatentGradient",
+ "BSZLatentHSVAImage",
+ "BSZLatentOffsetXL",
+ "BSZLatentRGBAImage",
+ "BSZLatentbuster",
+ "BSZPixelbuster",
+ "BSZPixelbusterHelp",
+ "BSZPrincipledConditioning",
+ "BSZPrincipledSampler",
+ "BSZPrincipledScale",
+ "BSZStrangeResample"
+ ],
+ {
+ "title_aux": "bsz-cui-extras"
+ }
+ ],
+ "https://github.com/BennyKok/comfyui-deploy": [
+ [
+ "ComfyUIDeployExternalImage",
+ "ComfyUIDeployExternalImageAlpha",
+ "ComfyUIDeployExternalText"
+ ],
+ {
+ "author": "BennyKok",
+ "description": "",
+ "nickname": "Comfy Deploy",
+ "title": "comfyui-deploy",
+ "title_aux": "ComfyUI Deploy"
+ }
+ ],
+ "https://github.com/Bikecicle/ComfyUI-Waveform-Extensions/raw/main/EXT_AudioManipulation.py": [
+ [
+ "BatchJoinAudio",
+ "CutAudio",
+ "DuplicateAudio",
+ "JoinAudio",
+ "ResampleAudio",
+ "ReverseAudio",
+ "StretchAudio"
+ ],
+ {
+ "title_aux": "Waveform Extensions"
+ }
+ ],
+ "https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb": [
+ [
+ "BNK_AddCLIPSDXLParams",
+ "BNK_AddCLIPSDXLRParams",
+ "BNK_CLIPTextEncodeAdvanced",
+ "BNK_CLIPTextEncodeSDXLAdvanced"
+ ],
+ {
+ "title_aux": "Advanced CLIP Text Encode"
+ }
+ ],
+ "https://github.com/BlenderNeko/ComfyUI_Cutoff": [
+ [
+ "BNK_CutoffBasePrompt",
+ "BNK_CutoffRegionsToConditioning",
+ "BNK_CutoffRegionsToConditioning_ADV",
+ "BNK_CutoffSetRegions"
+ ],
+ {
+ "title_aux": "ComfyUI Cutoff"
+ }
+ ],
+ "https://github.com/BlenderNeko/ComfyUI_Noise": [
+ [
+ "BNK_DuplicateBatchIndex",
+ "BNK_GetSigma",
+ "BNK_InjectNoise",
+ "BNK_NoisyLatentImage",
+ "BNK_SlerpLatent",
+ "BNK_Unsampler"
+ ],
+ {
+ "title_aux": "ComfyUI Noise"
+ }
+ ],
+ "https://github.com/BlenderNeko/ComfyUI_SeeCoder": [
+ [
+ "ConcatConditioning",
+ "SEECoderImageEncode"
+ ],
+ {
+ "title_aux": "SeeCoder [WIP]"
+ }
+ ],
+ "https://github.com/BlenderNeko/ComfyUI_TiledKSampler": [
+ [
+ "BNK_TiledKSampler",
+ "BNK_TiledKSamplerAdvanced"
+ ],
+ {
+ "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"
+ ],
+ {
+ "title_aux": "ComfyUI_Ib_CustomNodes"
+ }
+ ],
+ "https://github.com/Clybius/ComfyUI-Latent-Modifiers": [
+ [
+ "Latent Diffusion Mega Modifier"
+ ],
+ {
+ "title_aux": "ComfyUI-Latent-Modifiers"
+ }
+ ],
+ "https://github.com/CosmicLaca/ComfyUI_Primere_Nodes": [
+ [
+ "PrimereAnyDetailer",
+ "PrimereAnyOutput",
+ "PrimereCKPT",
+ "PrimereCKPTLoader",
+ "PrimereCLIPEncoder",
+ "PrimereClearPrompt",
+ "PrimereDynamicParser",
+ "PrimereEmbedding",
+ "PrimereEmbeddingHandler",
+ "PrimereEmbeddingKeywordMerger",
+ "PrimereHypernetwork",
+ "PrimereImageSegments",
+ "PrimereLCMSelector",
+ "PrimereLORA",
+ "PrimereLYCORIS",
+ "PrimereLatentNoise",
+ "PrimereLoraKeywordMerger",
+ "PrimereLoraStackMerger",
+ "PrimereLycorisKeywordMerger",
+ "PrimereLycorisStackMerger",
+ "PrimereMetaRead",
+ "PrimereMetaSave",
+ "PrimereModelKeyword",
+ "PrimereNetworkTagLoader",
+ "PrimerePrompt",
+ "PrimerePromptSwitch",
+ "PrimereResolution",
+ "PrimereResolutionMultiplier",
+ "PrimereSamplers",
+ "PrimereSeed",
+ "PrimereStepsCfg",
+ "PrimereStyleLoader",
+ "PrimereStylePile",
+ "PrimereTextOutput",
+ "PrimereVAE",
+ "PrimereVAELoader",
+ "PrimereVAESelector",
+ "PrimereVisualCKPT",
+ "PrimereVisualEmbedding",
+ "PrimereVisualHypernetwork",
+ "PrimereVisualLORA",
+ "PrimereVisualLYCORIS",
+ "PrimereVisualStyle"
+ ],
+ {
+ "title_aux": "Primere nodes for ComfyUI"
+ }
+ ],
+ "https://github.com/Danand/ComfyUI-ComfyCouple": [
+ [
+ "Attention couple",
+ "Comfy Couple"
+ ],
+ {
+ "author": "Rei D.",
+ "description": "If you want to draw two different characters together without blending their features, so you could try to check out this custom node.",
+ "nickname": "Danand",
+ "title": "Comfy Couple",
+ "title_aux": "ComfyUI-ComfyCouple"
+ }
+ ],
+ "https://github.com/Davemane42/ComfyUI_Dave_CustomNode": [
+ [
+ "ABGRemover",
+ "ConditioningStretch",
+ "ConditioningUpscale",
+ "MultiAreaConditioning",
+ "MultiLatentComposite"
+ ],
+ {
+ "title_aux": "Visual Area Conditioning / Latent composition"
+ }
+ ],
+ "https://github.com/Derfuu/Derfuu_ComfyUI_ModdedNodes": [
+ [
+ "ABSNode_DF",
+ "Absolute value",
+ "Ceil",
+ "CeilNode_DF",
+ "Conditioning area scale by ratio",
+ "ConditioningSetArea with tuples",
+ "ConditioningSetAreaEXT_DF",
+ "ConditioningSetArea_DF",
+ "CosNode_DF",
+ "Cosines",
+ "Divide",
+ "DivideNode_DF",
+ "EmptyLatentImage_DF",
+ "Float",
+ "Float debug print",
+ "Float2Tuple_DF",
+ "FloatDebugPrint_DF",
+ "FloatNode_DF",
+ "Floor",
+ "FloorNode_DF",
+ "Get image size",
+ "Get latent size",
+ "GetImageSize_DF",
+ "GetLatentSize_DF",
+ "Image scale by ratio",
+ "Image scale to side",
+ "ImageScale_Ratio_DF",
+ "ImageScale_Side_DF",
+ "Int debug print",
+ "Int to float",
+ "Int to tuple",
+ "Int2Float_DF",
+ "IntDebugPrint_DF",
+ "Integer",
+ "IntegerNode_DF",
+ "Latent Scale by ratio",
+ "Latent Scale to side",
+ "LatentComposite with tuples",
+ "LatentScale_Ratio_DF",
+ "LatentScale_Side_DF",
+ "MultilineStringNode_DF",
+ "Multiply",
+ "MultiplyNode_DF",
+ "PowNode_DF",
+ "Power",
+ "Random",
+ "RandomFloat_DF",
+ "SinNode_DF",
+ "Sinus",
+ "SqrtNode_DF",
+ "Square root",
+ "String debug print",
+ "StringNode_DF",
+ "Subtract",
+ "SubtractNode_DF",
+ "Sum",
+ "SumNode_DF",
+ "TanNode_DF",
+ "Tangent",
+ "Text",
+ "Text box",
+ "Tuple",
+ "Tuple debug print",
+ "Tuple multiply",
+ "Tuple swap",
+ "Tuple to floats",
+ "Tuple to ints",
+ "Tuple2Float_DF",
+ "TupleDebugPrint_DF",
+ "TupleNode_DF"
+ ],
+ {
+ "title_aux": "Derfuu_ComfyUI_ModdedNodes"
+ }
+ ],
+ "https://github.com/Electrofried/ComfyUI-OpenAINode": [
+ [
+ "OpenAINode"
+ ],
+ {
+ "title_aux": "OpenAINode"
+ }
+ ],
+ "https://github.com/EllangoK/ComfyUI-post-processing-nodes": [
+ [
+ "ArithmeticBlend",
+ "AsciiArt",
+ "Blend",
+ "Blur",
+ "CannyEdgeMask",
+ "ChromaticAberration",
+ "ColorCorrect",
+ "ColorTint",
+ "Dissolve",
+ "Dither",
+ "DodgeAndBurn",
+ "FilmGrain",
+ "Glow",
+ "HSVThresholdMask",
+ "KMeansQuantize",
+ "KuwaharaBlur",
+ "Parabolize",
+ "PencilSketch",
+ "PixelSort",
+ "Pixelize",
+ "Quantize",
+ "Sharpen",
+ "SineWave",
+ "Solarize",
+ "Vignette"
+ ],
+ {
+ "title_aux": "ComfyUI-post-processing-nodes"
+ }
+ ],
+ "https://github.com/Extraltodeus/LoadLoraWithTags": [
+ [
+ "LoraLoaderTagsQuery"
+ ],
+ {
+ "title_aux": "LoadLoraWithTags"
+ }
+ ],
+ "https://github.com/Extraltodeus/noise_latent_perlinpinpin": [
+ [
+ "NoisyLatentPerlin"
+ ],
+ {
+ "title_aux": "noise latent perlinpinpin"
+ }
+ ],
+ "https://github.com/Extraltodeus/sigmas_tools_and_the_golden_scheduler": [
+ [
+ "Get sigmas as float",
+ "Graph sigmas",
+ "Manual scheduler",
+ "Merge sigmas by average",
+ "Merge sigmas gradually",
+ "Multiply sigmas",
+ "Split and concatenate sigmas",
+ "The Golden Scheduler"
+ ],
+ {
+ "title_aux": "sigmas_tools_and_the_golden_scheduler"
+ }
+ ],
+ "https://github.com/Fannovel16/ComfyUI-Frame-Interpolation": [
+ [
+ "AMT VFI",
+ "CAIN VFI",
+ "EISAI VFI",
+ "FILM VFI",
+ "FLAVR VFI",
+ "GMFSS Fortuna VFI",
+ "IFRNet VFI",
+ "IFUnet VFI",
+ "KSampler Gradually Adding More Denoise (efficient)",
+ "M2M VFI",
+ "Make Interpolation State List",
+ "RIFE VFI",
+ "STMFNet VFI",
+ "Sepconv VFI"
+ ],
+ {
+ "title_aux": "ComfyUI Frame Interpolation"
+ }
+ ],
+ "https://github.com/Fannovel16/ComfyUI-Loopchain": [
+ [
+ "EmptyLatentImageLoop",
+ "FolderToImageStorage",
+ "ImageStorageExportLoop",
+ "ImageStorageImport",
+ "ImageStorageReset",
+ "LatentStorageExportLoop",
+ "LatentStorageImport",
+ "LatentStorageReset"
+ ],
+ {
+ "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-Video-Matting": [
+ [
+ "Robust Video Matting"
+ ],
+ {
+ "title_aux": "ComfyUI-Video-Matting"
+ }
+ ],
+ "https://github.com/Fannovel16/comfyui_controlnet_aux": [
+ [
+ "AIO_Preprocessor",
+ "AnimalPosePreprocessor",
+ "AnimeFace_SemSegPreprocessor",
+ "AnimeLineArtPreprocessor",
+ "BAE-NormalMapPreprocessor",
+ "BinaryPreprocessor",
+ "CannyEdgePreprocessor",
+ "ColorPreprocessor",
+ "DWPreprocessor",
+ "DensePosePreprocessor",
+ "FakeScribblePreprocessor",
+ "HEDPreprocessor",
+ "HintImageEnchance",
+ "ImageGenResolutionFromImage",
+ "ImageGenResolutionFromLatent",
+ "InpaintPreprocessor",
+ "LeReS-DepthMapPreprocessor",
+ "LineArtPreprocessor",
+ "LineartStandardPreprocessor",
+ "M-LSDPreprocessor",
+ "Manga2Anime_LineArt_Preprocessor",
+ "MediaPipe-FaceMeshPreprocessor",
+ "MiDaS-DepthMapPreprocessor",
+ "MiDaS-NormalMapPreprocessor",
+ "OneFormer-ADE20K-SemSegPreprocessor",
+ "OneFormer-COCO-SemSegPreprocessor",
+ "OpenposePreprocessor",
+ "PiDiNetPreprocessor",
+ "PixelPerfectResolution",
+ "SAMPreprocessor",
+ "ScribblePreprocessor",
+ "Scribble_XDoG_Preprocessor",
+ "SemSegPreprocessor",
+ "ShufflePreprocessor",
+ "TilePreprocessor",
+ "UniFormer-SemSegPreprocessor",
+ "Zoe-DepthMapPreprocessor"
+ ],
+ {
+ "author": "tstandley",
+ "title_aux": "ComfyUI's ControlNet Auxiliary Preprocessors"
+ }
+ ],
+ "https://github.com/Feidorian/feidorian-ComfyNodes": [
+ [],
+ {
+ "nodename_pattern": "^Feidorian_",
+ "title_aux": "feidorian-ComfyNodes"
+ }
+ ],
+ "https://github.com/Fictiverse/ComfyUI_Fictiverse": [
+ [
+ "Add Noise to Image with Mask",
+ "Color correction",
+ "Displace Image with Depth",
+ "Displace Images with Mask",
+ "Zoom Image with Depth"
+ ],
+ {
+ "title_aux": "ComfyUI Fictiverse Nodes"
+ }
+ ],
+ "https://github.com/FizzleDorf/ComfyUI-AIT": [
+ [
+ "AIT_Unet_Loader",
+ "AIT_VAE_Encode_Loader"
+ ],
+ {
+ "title_aux": "ComfyUI-AIT"
+ }
+ ],
+ "https://github.com/FizzleDorf/ComfyUI_FizzNodes": [
+ [
+ "AbsCosWave",
+ "AbsSinWave",
+ "BatchGLIGENSchedule",
+ "BatchPromptSchedule",
+ "BatchPromptScheduleEncodeSDXL",
+ "BatchPromptScheduleLatentInput",
+ "BatchPromptScheduleNodeFlowEnd",
+ "BatchPromptScheduleSDXLLatentInput",
+ "BatchStringSchedule",
+ "BatchValueSchedule",
+ "BatchValueScheduleLatentInput",
+ "CalculateFrameOffset",
+ "ConcatStringSingle",
+ "CosWave",
+ "FizzFrame",
+ "FizzFrameConcatenate",
+ "ImageBatchFromValueSchedule",
+ "Init FizzFrame",
+ "InvCosWave",
+ "InvSinWave",
+ "Lerp",
+ "PromptSchedule",
+ "PromptScheduleEncodeSDXL",
+ "PromptScheduleNodeFlow",
+ "PromptScheduleNodeFlowEnd",
+ "SawtoothWave",
+ "SinWave",
+ "SquareWave",
+ "StringConcatenate",
+ "StringSchedule",
+ "TriangleWave",
+ "ValueSchedule",
+ "convertKeyframeKeysToBatchKeys"
+ ],
+ {
+ "title_aux": "FizzNodes"
+ }
+ ],
+ "https://github.com/GMapeSplat/ComfyUI_ezXY": [
+ [
+ "ConcatenateString",
+ "ItemFromDropdown",
+ "IterationDriver",
+ "JoinImages",
+ "LineToConsole",
+ "NumberFromList",
+ "NumbersToList",
+ "PlotImages",
+ "StringFromList",
+ "StringToLabel",
+ "StringsToList",
+ "ezMath",
+ "ezXY_AssemblePlot",
+ "ezXY_Driver"
+ ],
+ {
+ "title_aux": "ezXY scripts and nodes"
+ }
+ ],
+ "https://github.com/GTSuya-Studio/ComfyUI-Gtsuya-Nodes": [
+ [
+ "Danbooru (ID)",
+ "Danbooru (Random)",
+ "Random File From Path",
+ "Replace Strings",
+ "Simple Wildcards",
+ "Simple Wildcards (Dir.)",
+ "Wildcards Nodes"
+ ],
+ {
+ "title_aux": "ComfyUI-GTSuya-Nodes"
+ }
+ ],
+ "https://github.com/Gourieff/comfyui-reactor-node": [
+ [
+ "ReActorFaceSwap",
+ "ReActorLoadFaceModel",
+ "ReActorSaveFaceModel"
+ ],
+ {
+ "title_aux": "ReActor Node for ComfyUI"
+ }
+ ],
+ "https://github.com/Haoming02/comfyui-diffusion-cg": [
+ [
+ "Hook Recenter",
+ "Hook Recenter XL",
+ "Normalization",
+ "NormalizationXL",
+ "Tensor Debug",
+ "Unhook Recenter"
+ ],
+ {
+ "title_aux": "ComfyUI Diffusion Color Grading"
+ }
+ ],
+ "https://github.com/Haoming02/comfyui-floodgate": [
+ [
+ "FloodGate"
+ ],
+ {
+ "title_aux": "ComfyUI Floodgate"
+ }
+ ],
+ "https://github.com/IDGallagher/ComfyUI-IG-Nodes": [
+ [
+ "IG Analyze SSIM",
+ "IG Explorer",
+ "IG Float",
+ "IG Folder",
+ "IG Int",
+ "IG Load Images",
+ "IG Multiply",
+ "IG Path Join",
+ "IG String"
+ ],
+ {
+ "author": "IDGallagher",
+ "description": "Custom nodes to aid in the exploration of Latent Space",
+ "nickname": "IG Interpolation Nodes",
+ "title": "IG Interpolation Nodes",
+ "title_aux": "IG Interpolation Nodes"
+ }
+ ],
+ "https://github.com/JPS-GER/ComfyUI_JPS-Nodes": [
+ [
+ "Conditioning Switch (JPS)",
+ "ControlNet Switch (JPS)",
+ "Crop Image Square (JPS)",
+ "Crop Image TargetSize (JPS)",
+ "Disable Enable Switch (JPS)",
+ "Enable Disable Switch (JPS)",
+ "Generation Settings (JPS)",
+ "Generation Settings Pipe (JPS)",
+ "Generation TXT IMG Settings (JPS)",
+ "Get Date Time String (JPS)",
+ "Get Image Size (JPS)",
+ "IP Adapter Settings (JPS)",
+ "IP Adapter Settings Pipe (JPS)",
+ "Image Switch (JPS)",
+ "Images Masks MultiPipe (JPS)",
+ "Integer Switch (JPS)",
+ "Largest Int (JPS)",
+ "Latent Switch (JPS)",
+ "Lora Loader (JPS)",
+ "Mask Switch (JPS)",
+ "Model Switch (JPS)",
+ "Multiply Float Float (JPS)",
+ "Multiply Int Float (JPS)",
+ "Multiply Int Int (JPS)",
+ "Resolution Multiply (JPS)",
+ "Revision Settings (JPS)",
+ "Revision Settings Pipe (JPS)",
+ "SDXL Basic Settings (JPS)",
+ "SDXL Basic Settings Pipe (JPS)",
+ "SDXL Fundamentals MultiPipe (JPS)",
+ "SDXL Prompt Handling (JPS)",
+ "SDXL Prompt Handling Plus (JPS)",
+ "SDXL Prompt Styler (JPS)",
+ "SDXL Recommended Resolution Calc (JPS)",
+ "SDXL Resolutions (JPS)",
+ "Sampler Scheduler Settings (JPS)",
+ "Substract Int Int (JPS)",
+ "Text Concatenate (JPS)",
+ "VAE Switch (JPS)"
+ ],
+ {
+ "author": "JPS",
+ "description": "Various nodes to handle SDXL Resolutions, SDXL Basic Settings, IP Adapter Settings, Revision Settings, SDXL Prompt Styler, Crop Image to Square, Crop Image to Target Size, Get Date-Time String, Resolution Multiply, Largest Integer, 5-to-1 Switches for Integer, Images, Latents, Conditioning, Model, VAE, ControlNet",
+ "nickname": "JPS Custom Nodes",
+ "title": "JPS Custom Nodes for ComfyUI",
+ "title_aux": "JPS Custom Nodes for ComfyUI"
+ }
+ ],
+ "https://github.com/Jcd1230/rembg-comfyui-node": [
+ [
+ "Image Remove Background (rembg)"
+ ],
+ {
+ "title_aux": "Rembg Background Removal Node for ComfyUI"
+ }
+ ],
+ "https://github.com/Jordach/comfy-plasma": [
+ [
+ "JDC_AutoContrast",
+ "JDC_BlendImages",
+ "JDC_BrownNoise",
+ "JDC_Contrast",
+ "JDC_EqualizeGrey",
+ "JDC_GaussianBlur",
+ "JDC_GreyNoise",
+ "JDC_Greyscale",
+ "JDC_ImageLoader",
+ "JDC_ImageLoaderMeta",
+ "JDC_PinkNoise",
+ "JDC_Plasma",
+ "JDC_PlasmaSampler",
+ "JDC_PowerImage",
+ "JDC_RandNoise",
+ "JDC_ResizeFactor"
+ ],
+ {
+ "title_aux": "comfy-plasma"
+ }
+ ],
+ "https://github.com/Kaharos94/ComfyUI-Saveaswebp": [
+ [
+ "Save_as_webp"
+ ],
+ {
+ "title_aux": "ComfyUI-Saveaswebp"
+ }
+ ],
+ "https://github.com/Kangkang625/ComfyUI-paint-by-example": [
+ [
+ "PaintbyExamplePipeLoader",
+ "PaintbyExampleSampler"
+ ],
+ {
+ "title_aux": "ComfyUI-Paint-by-Example"
+ }
+ ],
+ "https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet": [
+ [
+ "ACN_AdvancedControlNetApply",
+ "ACN_DefaultUniversalWeights",
+ "ACN_SparseCtrlIndexMethodNode",
+ "ACN_SparseCtrlLoaderAdvanced",
+ "ACN_SparseCtrlMergedLoaderAdvanced",
+ "ACN_SparseCtrlRGBPreprocessor",
+ "ACN_SparseCtrlSpreadMethodNode",
+ "ControlNetLoaderAdvanced",
+ "CustomControlNetWeights",
+ "CustomT2IAdapterWeights",
+ "DiffControlNetLoaderAdvanced",
+ "LatentKeyframe",
+ "LatentKeyframeBatchedGroup",
+ "LatentKeyframeGroup",
+ "LatentKeyframeTiming",
+ "LoadImagesFromDirectory",
+ "ScaledSoftControlNetWeights",
+ "ScaledSoftMaskedUniversalWeights",
+ "SoftControlNetWeights",
+ "SoftT2IAdapterWeights",
+ "TimestepKeyframe"
+ ],
+ {
+ "title_aux": "ComfyUI-Advanced-ControlNet"
+ }
+ ],
+ "https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved": [
+ [
+ "ADE_AnimateDiffCombine",
+ "ADE_AnimateDiffLoRALoader",
+ "ADE_AnimateDiffLoaderV1Advanced",
+ "ADE_AnimateDiffLoaderWithContext",
+ "ADE_AnimateDiffModelSettings",
+ "ADE_AnimateDiffModelSettingsAdvancedAttnStrengths",
+ "ADE_AnimateDiffModelSettingsSimple",
+ "ADE_AnimateDiffModelSettings_Release",
+ "ADE_AnimateDiffUniformContextOptions",
+ "ADE_AnimateDiffUniformContextOptionsExperimental",
+ "ADE_AnimateDiffUnload",
+ "ADE_EmptyLatentImageLarge",
+ "AnimateDiffLoaderV1",
+ "CheckpointLoaderSimpleWithNoiseSelect"
+ ],
+ {
+ "title_aux": "AnimateDiff Evolved"
+ }
+ ],
+ "https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite": [
+ [
+ "VHS_DuplicateImages",
+ "VHS_DuplicateLatents",
+ "VHS_GetImageCount",
+ "VHS_GetLatentCount",
+ "VHS_LoadAudio",
+ "VHS_LoadImages",
+ "VHS_LoadImagesPath",
+ "VHS_LoadVideo",
+ "VHS_LoadVideoPath",
+ "VHS_MergeImages",
+ "VHS_MergeLatents",
+ "VHS_SelectEveryNthImage",
+ "VHS_SelectEveryNthLatent",
+ "VHS_SplitImages",
+ "VHS_SplitLatents",
+ "VHS_VideoCombine"
+ ],
+ {
+ "title_aux": "ComfyUI-VideoHelperSuite"
+ }
+ ],
+ "https://github.com/LEv145/images-grid-comfy-plugin": [
+ [
+ "GridAnnotation",
+ "ImageCombine",
+ "ImagesGridByColumns",
+ "ImagesGridByRows",
+ "LatentCombine"
+ ],
+ {
+ "title_aux": "ImagesGrid"
+ }
+ ],
+ "https://github.com/Lerc/canvas_tab": [
+ [
+ "Canvas_Tab",
+ "Send_To_Editor"
+ ],
+ {
+ "author": "Lerc",
+ "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.",
+ "nickname": "Canvas Tab",
+ "title": "Canvas Tab",
+ "title_aux": "Canvas Tab"
+ }
+ ],
+ "https://github.com/Limitex/ComfyUI-Calculation": [
+ [
+ "CenterCalculation",
+ "CreateQRCode"
+ ],
+ {
+ "title_aux": "ComfyUI-Calculation"
+ }
+ ],
+ "https://github.com/Limitex/ComfyUI-Diffusers": [
+ [
+ "DiffusersClipTextEncode",
+ "DiffusersModelMakeup",
+ "DiffusersPipelineLoader",
+ "DiffusersSampler",
+ "DiffusersSaveImage",
+ "DiffusersSchedulerLoader",
+ "DiffusersVaeLoader",
+ "StreamDiffusionCreateStream",
+ "StreamDiffusionFastSampler",
+ "StreamDiffusionSampler",
+ "StreamDiffusionWarmup"
+ ],
+ {
+ "title_aux": "ComfyUI-Diffusers"
+ }
+ ],
+ "https://github.com/LonicaMewinsky/ComfyUI-MakeFrame": [
+ [
+ "BreakFrames",
+ "BreakGrid",
+ "GetKeyFrames",
+ "MakeGrid",
+ "RandomImageFromDir"
+ ],
+ {
+ "title_aux": "ComfyBreakAnim"
+ }
+ ],
+ "https://github.com/LonicaMewinsky/ComfyUI-RawSaver": [
+ [
+ "SaveTifImage"
+ ],
+ {
+ "title_aux": "ComfyUI-RawSaver"
+ }
+ ],
+ "https://github.com/M1kep/ComfyLiterals": [
+ [
+ "Checkpoint",
+ "Float",
+ "Int",
+ "KepStringLiteral",
+ "Lora",
+ "Operation",
+ "String"
+ ],
+ {
+ "title_aux": "ComfyLiterals"
+ }
+ ],
+ "https://github.com/M1kep/ComfyUI-KepOpenAI": [
+ [
+ "KepOpenAI_ImageWithPrompt"
+ ],
+ {
+ "title_aux": "ComfyUI-KepOpenAI"
+ }
+ ],
+ "https://github.com/M1kep/ComfyUI-OtherVAEs": [
+ [
+ "OtherVAE_Taesd"
+ ],
+ {
+ "title_aux": "ComfyUI-OtherVAEs"
+ }
+ ],
+ "https://github.com/M1kep/Comfy_KepKitchenSink": [
+ [
+ "KepRotateImage"
+ ],
+ {
+ "title_aux": "Comfy_KepKitchenSink"
+ }
+ ],
+ "https://github.com/M1kep/Comfy_KepListStuff": [
+ [
+ "Empty Images",
+ "Image Overlay",
+ "ImageListLoader",
+ "Join Float Lists",
+ "Join Image Lists",
+ "KepStringList",
+ "KepStringListFromNewline",
+ "Kep_JoinListAny",
+ "Kep_RepeatList",
+ "Kep_ReverseList",
+ "Kep_VariableImageBuilder",
+ "List Length",
+ "Range(Num Steps) - Float",
+ "Range(Num Steps) - Int",
+ "Range(Step) - Float",
+ "Range(Step) - Int",
+ "Stack Images",
+ "XYAny",
+ "XYImage"
+ ],
+ {
+ "title_aux": "Comfy_KepListStuff"
+ }
+ ],
+ "https://github.com/M1kep/Comfy_KepMatteAnything": [
+ [
+ "MatteAnything_DinoBoxes",
+ "MatteAnything_GenerateVITMatte",
+ "MatteAnything_InitSamPredictor",
+ "MatteAnything_LoadDINO",
+ "MatteAnything_LoadVITMatteModel",
+ "MatteAnything_SAMLoader",
+ "MatteAnything_SAMMaskFromBoxes",
+ "MatteAnything_ToTrimap"
+ ],
+ {
+ "title_aux": "Comfy_KepMatteAnything"
+ }
+ ],
+ "https://github.com/M1kep/KepPromptLang": [
+ [
+ "Build Gif",
+ "Special CLIP Loader"
+ ],
+ {
+ "title_aux": "KepPromptLang"
+ }
+ ],
+ "https://github.com/MNeMoNiCuZ/ComfyUI-mnemic-nodes": [
+ [
+ "Save Text File_mne"
+ ],
+ {
+ "title_aux": "ComfyUI-mnemic-nodes"
+ }
+ ],
+ "https://github.com/ManglerFTW/ComfyI2I": [
+ [
+ "Color Transfer",
+ "Combine and Paste",
+ "Inpaint Segments",
+ "Mask Ops"
+ ],
+ {
+ "author": "ManglerFTW",
+ "title": "ComfyI2I",
+ "title_aux": "ComfyI2I"
+ }
+ ],
+ "https://github.com/MitoshiroPJ/comfyui_slothful_attention": [
+ [
+ "NearSightedAttention",
+ "NearSightedAttentionSimple",
+ "NearSightedTile",
+ "SlothfulAttention"
+ ],
+ {
+ "title_aux": "ComfyUI Slothful Attention"
+ }
+ ],
+ "https://github.com/NicholasMcCarthy/ComfyUI_TravelSuite": [
+ [
+ "LatentTravel"
+ ],
+ {
+ "title_aux": "ComfyUI_TravelSuite"
+ }
+ ],
+ "https://github.com/NimaNzrii/comfyui-photoshop": [
+ [
+ "PhotoshopToComfyUI"
+ ],
+ {
+ "title_aux": "comfyui-photoshop"
+ }
+ ],
+ "https://github.com/NimaNzrii/comfyui-popup_preview": [
+ [
+ "PreviewPopup"
+ ],
+ {
+ "title_aux": "comfyui-popup_preview"
+ }
+ ],
+ "https://github.com/Niutonian/ComfyUi-NoodleWebcam": [
+ [
+ "WebcamNode"
+ ],
+ {
+ "title_aux": "ComfyUi-NoodleWebcam"
+ }
+ ],
+ "https://github.com/NotHarroweD/Harronode": [
+ [
+ "Harronode"
+ ],
+ {
+ "author": "HarroweD and quadmoon (https://github.com/traugdor)",
+ "description": "This extension to ComfyUI will build a prompt for the Harrlogos LoRA for SDXL.",
+ "nickname": "Harronode",
+ "nodename_pattern": "Harronode",
+ "title": "Harrlogos Prompt Builder Node",
+ "title_aux": "Harronode"
+ }
+ ],
+ "https://github.com/Nourepide/ComfyUI-Allor": [
+ [
+ "AlphaChanelAdd",
+ "AlphaChanelAddByMask",
+ "AlphaChanelAsMask",
+ "AlphaChanelRemove",
+ "AlphaChanelRestore",
+ "ClipClamp",
+ "ClipVisionClamp",
+ "ClipVisionOutputClamp",
+ "ConditioningClamp",
+ "ControlNetClamp",
+ "GligenClamp",
+ "ImageBatchCopy",
+ "ImageBatchFork",
+ "ImageBatchGet",
+ "ImageBatchJoin",
+ "ImageBatchPermute",
+ "ImageBatchRemove",
+ "ImageClamp",
+ "ImageCompositeAbsolute",
+ "ImageCompositeAbsoluteByContainer",
+ "ImageCompositeRelative",
+ "ImageCompositeRelativeByContainer",
+ "ImageContainer",
+ "ImageContainerInheritanceAdd",
+ "ImageContainerInheritanceMax",
+ "ImageContainerInheritanceScale",
+ "ImageContainerInheritanceSum",
+ "ImageDrawArc",
+ "ImageDrawArcByContainer",
+ "ImageDrawChord",
+ "ImageDrawChordByContainer",
+ "ImageDrawEllipse",
+ "ImageDrawEllipseByContainer",
+ "ImageDrawLine",
+ "ImageDrawLineByContainer",
+ "ImageDrawPieslice",
+ "ImageDrawPiesliceByContainer",
+ "ImageDrawPolygon",
+ "ImageDrawRectangle",
+ "ImageDrawRectangleByContainer",
+ "ImageDrawRectangleRounded",
+ "ImageDrawRectangleRoundedByContainer",
+ "ImageEffectsAdjustment",
+ "ImageEffectsGrayscale",
+ "ImageEffectsLensBokeh",
+ "ImageEffectsLensChromaticAberration",
+ "ImageEffectsLensOpticAxis",
+ "ImageEffectsLensVignette",
+ "ImageEffectsLensZoomBurst",
+ "ImageEffectsNegative",
+ "ImageEffectsSepia",
+ "ImageFilterBilateralBlur",
+ "ImageFilterBlur",
+ "ImageFilterBoxBlur",
+ "ImageFilterContour",
+ "ImageFilterDetail",
+ "ImageFilterEdgeEnhance",
+ "ImageFilterEdgeEnhanceMore",
+ "ImageFilterEmboss",
+ "ImageFilterFindEdges",
+ "ImageFilterGaussianBlur",
+ "ImageFilterGaussianBlurAdvanced",
+ "ImageFilterMax",
+ "ImageFilterMedianBlur",
+ "ImageFilterMin",
+ "ImageFilterMode",
+ "ImageFilterRank",
+ "ImageFilterSharpen",
+ "ImageFilterSmooth",
+ "ImageFilterSmoothMore",
+ "ImageFilterStackBlur",
+ "ImageNoiseBeta",
+ "ImageNoiseBinomial",
+ "ImageNoiseBytes",
+ "ImageNoiseGaussian",
+ "ImageSegmentation",
+ "ImageSegmentationCustom",
+ "ImageSegmentationCustomAdvanced",
+ "ImageText",
+ "ImageTextMultiline",
+ "ImageTextMultilineOutlined",
+ "ImageTextOutlined",
+ "ImageTransformCropAbsolute",
+ "ImageTransformCropCorners",
+ "ImageTransformCropRelative",
+ "ImageTransformPaddingAbsolute",
+ "ImageTransformPaddingRelative",
+ "ImageTransformResizeAbsolute",
+ "ImageTransformResizeClip",
+ "ImageTransformResizeRelative",
+ "ImageTransformRotate",
+ "ImageTransformTranspose",
+ "LatentClamp",
+ "MaskClamp",
+ "ModelClamp",
+ "StyleModelClamp",
+ "UpscaleModelClamp",
+ "VaeClamp"
+ ],
+ {
+ "title_aux": "Allor Plugin"
+ }
+ ],
+ "https://github.com/Nuked88/ComfyUI-N-Nodes": [
+ [
+ "DynamicPrompt",
+ "Float Variable",
+ "FrameInterpolator",
+ "GPT Loader Simple",
+ "GPTSampler",
+ "Integer Variable",
+ "LoadFramesFromFolder",
+ "LoadVideo",
+ "SaveVideo",
+ "SetMetadataForSaveVideo",
+ "String Variable"
+ ],
+ {
+ "title_aux": "ComfyUI-N-Nodes"
+ }
+ ],
+ "https://github.com/Off-Live/ComfyUI-off-suite": [
+ [
+ "Cached Image Load From URL",
+ "Crop Center wigh SEGS",
+ "Crop Center with SEGS",
+ "GW Number Formatting",
+ "Image Crop Fit",
+ "Image Resize Fit",
+ "OFF SEGS to Image",
+ "Watermarking"
+ ],
+ {
+ "title_aux": "ComfyUI-off-suite"
+ }
+ ],
+ "https://github.com/Onierous/QRNG_Node_ComfyUI/raw/main/qrng_node.py": [
+ [
+ "QRNG_Node_CSV"
+ ],
+ {
+ "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",
+ "CombinePrompt",
+ "MultiLoraLoader",
+ "RandomPrompt"
+ ],
+ {
+ "title_aux": "ComfyUI-Malefish-Custom-Scripts"
+ }
+ ],
+ "https://github.com/Pfaeff/pfaeff-comfyui": [
+ [
+ "AstropulsePixelDetector",
+ "BackgroundRemover",
+ "ImagePadForBetterOutpaint",
+ "Inpainting",
+ "InpaintingPipelineLoader"
+ ],
+ {
+ "title_aux": "pfaeff-comfyui"
+ }
+ ],
+ "https://github.com/RenderRift/ComfyUI-RenderRiftNodes": [
+ [
+ "AnalyseMetadata",
+ "DateIntegerNode",
+ "DisplayMetaOptions",
+ "LoadImageWithMeta",
+ "MetadataOverlayNode",
+ "VideoPathMetaExtraction"
+ ],
+ {
+ "title_aux": "ComfyUI-RenderRiftNodes"
+ }
+ ],
+ "https://github.com/Ryuukeisyou/comfyui_face_parsing": [
+ [
+ "BBoxListItemSelect(FaceParsing)",
+ "BBoxResize(FaceParsing)",
+ "ColorAdjust(FaceParsing)",
+ "FaceBBoxDetect(FaceParsing)",
+ "FaceBBoxDetectorLoader(FaceParsing)",
+ "FaceParse(FaceParsing)",
+ "FaceParsingModelLoader(FaceParsing)",
+ "FaceParsingProcessorLoader(FaceParsing)",
+ "FaceParsingResultsParser(FaceParsing)",
+ "GuidedFilter(FaceParsing)",
+ "ImageCropWithBBox(FaceParsing)",
+ "ImageInsertWithBBox(FaceParsing)",
+ "ImageListSelect(FaceParsing)",
+ "ImagePadWithBBox(FaceParsing)",
+ "ImageResizeCalculator(FaceParsing)",
+ "ImageSize(FaceParsing)",
+ "MaskComposite(FaceParsing)",
+ "MaskListComposite(FaceParsing)",
+ "MaskListSelect(FaceParsing)"
+ ],
+ {
+ "title_aux": "comfyui_face_parsing"
+ }
+ ],
+ "https://github.com/SLAPaper/ComfyUI-Image-Selector": [
+ [
+ "ImageDuplicator",
+ "ImageSelector",
+ "LatentDuplicator",
+ "LatentSelector"
+ ],
+ {
+ "title_aux": "ComfyUI-Image-Selector"
+ }
+ ],
+ "https://github.com/SOELexicon/ComfyUI-LexMSDBNodes": [
+ [
+ "MSSqlSelectNode",
+ "MSSqlTableNode"
+ ],
+ {
+ "title_aux": "LexMSDBNodes"
+ }
+ ],
+ "https://github.com/SOELexicon/ComfyUI-LexTools": [
+ [
+ "AgeClassifierNode",
+ "ArtOrHumanClassifierNode",
+ "DocumentClassificationNode",
+ "FoodCategoryClassifierNode",
+ "ImageAspectPadNode",
+ "ImageCaptioning",
+ "ImageFilterByFloatScoreNode",
+ "ImageFilterByIntScoreNode",
+ "ImageQualityScoreNode",
+ "ImageRankingNode",
+ "ImageScaleToMin",
+ "MD5ImageHashNode",
+ "SamplerPropertiesNode",
+ "ScoreConverterNode",
+ "SeedIncrementerNode",
+ "SegformerNode",
+ "SegformerNodeMasks",
+ "SegformerNodeMergeSegments",
+ "StepCfgIncrementNode"
+ ],
+ {
+ "title_aux": "ComfyUI-LexTools"
+ }
+ ],
+ "https://github.com/SadaleNet/CLIPTextEncodeA1111-ComfyUI/raw/master/custom_nodes/clip_text_encoder_a1111.py": [
+ [
+ "CLIPTextEncodeA1111",
+ "RerouteTextForCLIPTextEncodeA1111"
+ ],
+ {
+ "title_aux": "ComfyUI A1111-like Prompt Custom Node Solution"
+ }
+ ],
+ "https://github.com/Scholar01/ComfyUI-Keyframe": [
+ [
+ "KeyframeApply",
+ "KeyframeInterpolationPart",
+ "KeyframePart"
+ ],
+ {
+ "title_aux": "SComfyUI-Keyframe"
+ }
+ ],
+ "https://github.com/SeargeDP/SeargeSDXL": [
+ [
+ "SeargeAdvancedParameters",
+ "SeargeCheckpointLoader",
+ "SeargeConditionMixing",
+ "SeargeConditioningMuxer2",
+ "SeargeConditioningMuxer5",
+ "SeargeConditioningParameters",
+ "SeargeControlnetAdapterV2",
+ "SeargeControlnetModels",
+ "SeargeCustomAfterUpscaling",
+ "SeargeCustomAfterVaeDecode",
+ "SeargeCustomPromptMode",
+ "SeargeDebugPrinter",
+ "SeargeEnablerInputs",
+ "SeargeFloatConstant",
+ "SeargeFloatMath",
+ "SeargeFloatPair",
+ "SeargeFreeU",
+ "SeargeGenerated1",
+ "SeargeGenerationParameters",
+ "SeargeHighResolution",
+ "SeargeImage2ImageAndInpainting",
+ "SeargeImageAdapterV2",
+ "SeargeImageSave",
+ "SeargeImageSaving",
+ "SeargeInput1",
+ "SeargeInput2",
+ "SeargeInput3",
+ "SeargeInput4",
+ "SeargeInput5",
+ "SeargeInput6",
+ "SeargeInput7",
+ "SeargeIntegerConstant",
+ "SeargeIntegerMath",
+ "SeargeIntegerPair",
+ "SeargeIntegerScaler",
+ "SeargeLatentMuxer3",
+ "SeargeLoraLoader",
+ "SeargeLoras",
+ "SeargeMagicBox",
+ "SeargeModelSelector",
+ "SeargeOperatingMode",
+ "SeargeOutput1",
+ "SeargeOutput2",
+ "SeargeOutput3",
+ "SeargeOutput4",
+ "SeargeOutput5",
+ "SeargeOutput6",
+ "SeargeOutput7",
+ "SeargeParameterProcessor",
+ "SeargePipelineStart",
+ "SeargePipelineTerminator",
+ "SeargePreviewImage",
+ "SeargePromptAdapterV2",
+ "SeargePromptCombiner",
+ "SeargePromptStyles",
+ "SeargePromptText",
+ "SeargeSDXLBasePromptEncoder",
+ "SeargeSDXLImage2ImageSampler",
+ "SeargeSDXLImage2ImageSampler2",
+ "SeargeSDXLPromptEncoder",
+ "SeargeSDXLRefinerPromptEncoder",
+ "SeargeSDXLSampler",
+ "SeargeSDXLSampler2",
+ "SeargeSDXLSamplerV3",
+ "SeargeSamplerAdvanced",
+ "SeargeSamplerInputs",
+ "SeargeSaveFolderInputs",
+ "SeargeSeparator",
+ "SeargeStylePreprocessor",
+ "SeargeTextInputV2",
+ "SeargeUpscaleModelLoader",
+ "SeargeUpscaleModels",
+ "SeargeVAELoader"
+ ],
+ {
+ "title_aux": "SeargeSDXL"
+ }
+ ],
+ "https://github.com/Ser-Hilary/SDXL_sizing/raw/main/conditioning_sizing_for_SDXL.py": [
+ [
+ "get_aspect_from_image",
+ "get_aspect_from_ints",
+ "sizing_node",
+ "sizing_node_basic",
+ "sizing_node_unparsed"
+ ],
+ {
+ "title_aux": "SDXL_sizing"
+ }
+ ],
+ "https://github.com/Smuzzies/comfyui_chatbox_overlay/raw/main/chatbox_overlay.py": [
+ [
+ "Chatbox Overlay"
+ ],
+ {
+ "title_aux": "Chatbox Overlay node for ComfyUI"
+ }
+ ],
+ "https://github.com/SoftMeng/ComfyUI_Mexx_Poster": [
+ [
+ "ComfyUI_Mexx_Poster"
+ ],
+ {
+ "title_aux": "ComfyUI_Mexx_Poster"
+ }
+ ],
+ "https://github.com/SoftMeng/ComfyUI_Mexx_Styler": [
+ [
+ "MexxSDXLPromptStyler",
+ "MexxSDXLPromptStylerAdvanced"
+ ],
+ {
+ "title_aux": "ComfyUI_Mexx_Styler"
+ }
+ ],
+ "https://github.com/SpaceKendo/ComfyUI-svd_txt2vid": [
+ [
+ "SVD_txt2vid_ConditioningwithLatent"
+ ],
+ {
+ "title_aux": "Text to video for Stable Video Diffusion in ComfyUI"
+ }
+ ],
+ "https://github.com/Stability-AI/stability-ComfyUI-nodes": [
+ [
+ "ColorBlend",
+ "ControlLoraSave",
+ "GetImageSize"
+ ],
+ {
+ "title_aux": "stability-ComfyUI-nodes"
+ }
+ ],
+ "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes": [
+ [
+ "CR 3D Camera Drone",
+ "CR 3D Camera Static",
+ "CR 3D Polygon",
+ "CR 3D Solids",
+ "CR ASCII Pattern",
+ "CR Add Annotation",
+ "CR Alternate Latents",
+ "CR Apply Annotations",
+ "CR Apply ControlNet",
+ "CR Apply LoRA Stack",
+ "CR Apply Model Merge",
+ "CR Apply Multi Upscale",
+ "CR Apply Multi-ControlNet",
+ "CR Arabic Text RTL",
+ "CR Aspect Ratio",
+ "CR Aspect Ratio Banners",
+ "CR Aspect Ratio SDXL",
+ "CR Batch Images From List",
+ "CR Batch Process Switch",
+ "CR Binary Pattern",
+ "CR Binary To List",
+ "CR Central Schedule",
+ "CR Check Job Complete",
+ "CR Checker Pattern",
+ "CR Clamp Value",
+ "CR Clip Input Switch",
+ "CR Color Bars",
+ "CR Color Gradient",
+ "CR Color Panel",
+ "CR Color Tint",
+ "CR Combine Schedules",
+ "CR Comic Panel Templates",
+ "CR Comic Panel Templates (Advanced)",
+ "CR Comic Panel Templates Advanced",
+ "CR Composite Text",
+ "CR Conditioning Input Switch",
+ "CR Conditioning Mixer",
+ "CR Continuous Rotation",
+ "CR Continuous Track",
+ "CR Continuous Zoom",
+ "CR ControlNet Input Switch",
+ "CR Current Frame",
+ "CR Cycle Images",
+ "CR Cycle Images Simple",
+ "CR Cycle LoRAs",
+ "CR Cycle Models",
+ "CR Cycle Styles",
+ "CR Cycle Text",
+ "CR Cycle Text Simple",
+ "CR Debatch Frames",
+ "CR Display Font",
+ "CR Draw OBJ",
+ "CR Draw Perspective Text",
+ "CR Draw Pie",
+ "CR Draw Shape",
+ "CR Draw Text",
+ "CR Encode Scheduled Prompts",
+ "CR Feathered Border",
+ "CR Float Range List",
+ "CR Float To Integer",
+ "CR Float To String",
+ "CR Font File List",
+ "CR Gradient Float",
+ "CR Gradient Integer",
+ "CR Halftone Filter",
+ "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 List",
+ "CR Image List Simple",
+ "CR Image Output",
+ "CR Image Panel",
+ "CR Image Pipe Edit",
+ "CR Image Pipe In",
+ "CR Image Pipe Out",
+ "CR Image Size",
+ "CR Image Transition",
+ "CR Image XY Panel",
+ "CR Img2Img Process Switch",
+ "CR Increment Float",
+ "CR Increment Integer",
+ "CR Index",
+ "CR Index Increment",
+ "CR Index Multiply",
+ "CR Index Reset",
+ "CR Input Text List",
+ "CR Integer Multiple",
+ "CR Integer Range List",
+ "CR Integer To String",
+ "CR Interpolate Latents",
+ "CR Interpolate Prompt Weights",
+ "CR Interpolate Rotation",
+ "CR Interpolate Track",
+ "CR Interpolate Zoom",
+ "CR Intertwine Lists",
+ "CR Job Current Frame",
+ "CR Job List",
+ "CR Job Scheduler",
+ "CR Keyframe List",
+ "CR Latent Batch Size",
+ "CR Latent Input Switch",
+ "CR List Schedule",
+ "CR LoRA List",
+ "CR LoRA Stack",
+ "CR Load Animation Frames",
+ "CR Load Flow Frames",
+ "CR Load Image List",
+ "CR Load Image List Plus",
+ "CR Load LoRA",
+ "CR Load Prompt Style",
+ "CR Load Schedule From File",
+ "CR Load Scheduled ControlNets",
+ "CR Load Scheduled LoRAs",
+ "CR Load Scheduled Models",
+ "CR Load Text List",
+ "CR Load Workflow",
+ "CR Load XY Annotation From File",
+ "CR Mask Text",
+ "CR Model Input Switch",
+ "CR Model List",
+ "CR Model Merge Stack",
+ "CR Module Input",
+ "CR Module Output",
+ "CR Module Pipe Loader",
+ "CR Multi Upscale Stack",
+ "CR Multi-ControlNet Stack",
+ "CR Multi-Panel Meme Template",
+ "CR Multiline Text",
+ "CR Output Flow Frames",
+ "CR Output Schedule To File",
+ "CR Overlay Text",
+ "CR Overlay Transparent Image",
+ "CR Page Layout",
+ "CR Pipe Switch",
+ "CR Polygons",
+ "CR Popular Meme Templates",
+ "CR Prompt List",
+ "CR Prompt List Keyframes",
+ "CR Prompt Scheduler",
+ "CR Prompt Text",
+ "CR Prompt Weight Scheduler",
+ "CR Radial Gradient",
+ "CR Radial Gradient Map",
+ "CR Random Hex Color",
+ "CR Random LoRA Stack",
+ "CR Random Multiline Colors",
+ "CR Random Multiline Values",
+ "CR Random Panel Codes",
+ "CR Random RGB",
+ "CR Random RGB Gradient",
+ "CR Random Shape Pattern",
+ "CR Random Weight LoRA",
+ "CR SD1.5 Aspect Ratio",
+ "CR SDXL Aspect Ratio",
+ "CR SDXL Base Prompt Encoder",
+ "CR SDXL Prompt Mix Presets",
+ "CR SDXL Style Text",
+ "CR Save Text To File",
+ "CR Schedule Camera Movements",
+ "CR Schedule ControlNets",
+ "CR Schedule Input Switch",
+ "CR Schedule Styles",
+ "CR Schedule To ScheduleList",
+ "CR Seed",
+ "CR Seed to Int",
+ "CR Select Model",
+ "CR Set Value On Boolean",
+ "CR Simple Annotations",
+ "CR Simple Banner",
+ "CR Simple Binary Pattern",
+ "CR Simple Binary Pattern Simple",
+ "CR Simple Image Compare",
+ "CR Simple Image Watermark",
+ "CR Simple Meme Template",
+ "CR Simple Prompt List",
+ "CR Simple Prompt List Keyframes",
+ "CR Simple Prompt Scheduler",
+ "CR Simple Schedule",
+ "CR Simple Text Panel",
+ "CR Simple Text Scheduler",
+ "CR Simple Text Watermark",
+ "CR Simple Titles",
+ "CR Simple Value Scheduler",
+ "CR Spawn Workflow Instance",
+ "CR Split String",
+ "CR Starburst Colors",
+ "CR Starburst Lines",
+ "CR String To Combo",
+ "CR String To Number",
+ "CR Strobe Images",
+ "CR Style Bars",
+ "CR Style List",
+ "CR Switch Model and CLIP",
+ "CR System TrueType Font",
+ "CR Text Input Switch",
+ "CR Text Input Switch (4 way)",
+ "CR Text List",
+ "CR Text List Cross Join",
+ "CR Text List Simple",
+ "CR Text List To String",
+ "CR Text Scheduler",
+ "CR Thumbnail Preview",
+ "CR Trigger",
+ "CR Upscale Image",
+ "CR VAE Input Switch",
+ "CR Value",
+ "CR Value Scheduler",
+ "CR Vignette Filter",
+ "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 XYZ Interpolate",
+ "CR XYZ List"
+ ],
+ {
+ "author": "Suzie1",
+ "description": "165 custom nodes for Graphics, Animation, IO, Aspect Ratio, Model Merge, ControlNet, LoRA, XY Grid, and Utilities.",
+ "nickname": "Comfyroll Studio",
+ "title": "Comfyroll Studio",
+ "title_aux": "ComfyUI_Comfyroll_CustomNodes"
+ }
+ ],
+ "https://github.com/Sxela/ComfyWarp": [
+ [
+ "ExtractOpticalFlow",
+ "LoadFrame",
+ "LoadFrameFromDataset",
+ "LoadFrameFromFolder",
+ "LoadFramePairFromDataset",
+ "LoadFrameSequence",
+ "MakeFrameDataset",
+ "MixConsistencyMaps",
+ "OffsetNumber",
+ "ResizeToFit",
+ "SaveFrame",
+ "WarpFrame"
+ ],
+ {
+ "title_aux": "ComfyWarp"
+ }
+ ],
+ "https://github.com/TGu-97/ComfyUI-TGu-utils": [
+ [
+ "MPNReroute",
+ "MPNSwitch",
+ "PNSwitch"
+ ],
+ {
+ "title_aux": "TGu Utilities"
+ }
+ ],
+ "https://github.com/THtianhao/ComfyUI-FaceChain": [
+ [
+ "FCStyleLoraLoad",
+ "FC_CropAndPaste",
+ "FC_CropBottom",
+ "FC_CropFace",
+ "FC_CropMask",
+ "FC_FaceDetection",
+ "FC_FaceFusion",
+ "FC_MaskOP",
+ "FC_ReplaceImage",
+ "FC_Segment",
+ "FC_StyleLoraLoad"
+ ],
+ {
+ "title_aux": "ComfyUI-FaceChain"
+ }
+ ],
+ "https://github.com/THtianhao/ComfyUI-Portrait-Maker": [
+ [
+ "PM_BoxCropImage",
+ "PM_ColorTransfer",
+ "PM_ExpandMaskBox",
+ "PM_FaceFusion",
+ "PM_FaceShapMatch",
+ "PM_FaceSkin",
+ "PM_GetImageInfo",
+ "PM_ImageResizeTarget",
+ "PM_ImageScaleShort",
+ "PM_MakeUpTransfer",
+ "PM_MaskDilateErode",
+ "PM_MaskMerge2Image",
+ "PM_PortraitEnhancement",
+ "PM_RatioMerge2Image",
+ "PM_ReplaceBoxImg",
+ "PM_RetinaFace",
+ "PM_Similarity",
+ "PM_SkinRetouching",
+ "PM_SuperColorTransfer",
+ "PM_SuperMakeUpTransfer"
+ ],
+ {
+ "title_aux": "ComfyUI-Portrait-Maker"
+ }
+ ],
+ "https://github.com/TRI3D-LC/tri3d-comfyui-nodes": [
+ [
+ "tri3d-atr-parse",
+ "tri3d-atr-parse-batch",
+ "tri3d-dwpose",
+ "tri3d-extract-hand",
+ "tri3d-extract-parts-batch",
+ "tri3d-extract-parts-batch2",
+ "tri3d-extract-parts-mask-batch",
+ "tri3d-fuzzification",
+ "tri3d-interaction-canny",
+ "tri3d-pose-adaption",
+ "tri3d-pose-to-image",
+ "tri3d-position-hands",
+ "tri3d-position-parts-batch",
+ "tri3d-skin-feathered-padded-mask",
+ "tri3d-swap-pixels"
+ ],
+ {
+ "title_aux": "tri3d-comfyui-nodes"
+ }
+ ],
+ "https://github.com/TeaCrab/ComfyUI-TeaNodes": [
+ [
+ "TC_ColorFill",
+ "TC_EqualizeCLAHE",
+ "TC_ImageResize",
+ "TC_ImageScale",
+ "TC_MaskBG_DIS",
+ "TC_RandomColorFill",
+ "TC_SizeApproximation"
+ ],
+ {
+ "title_aux": "ComfyUI-TeaNodes"
+ }
+ ],
+ "https://github.com/TheBarret/ZSuite": [
+ [
+ "ZSuite: Prompter",
+ "ZSuite: RF Noise",
+ "ZSuite: SeedMod"
+ ],
+ {
+ "title_aux": "ZSuite"
+ }
+ ],
+ "https://github.com/TinyTerra/ComfyUI_tinyterraNodes": [
+ [
+ "ttN busIN",
+ "ttN busOUT",
+ "ttN compareInput",
+ "ttN concat",
+ "ttN debugInput",
+ "ttN float",
+ "ttN hiresfixScale",
+ "ttN imageOutput",
+ "ttN imageREMBG",
+ "ttN int",
+ "ttN multiModelMerge",
+ "ttN pipe2BASIC",
+ "ttN pipe2DETAILER",
+ "ttN pipeEDIT",
+ "ttN pipeEncodeConcat",
+ "ttN pipeIN",
+ "ttN pipeKSampler",
+ "ttN pipeKSamplerAdvanced",
+ "ttN pipeKSamplerSDXL",
+ "ttN pipeLoader",
+ "ttN pipeLoaderSDXL",
+ "ttN pipeLoraStack",
+ "ttN pipeOUT",
+ "ttN seed",
+ "ttN seedDebug",
+ "ttN text",
+ "ttN text3BOX_3WAYconcat",
+ "ttN text7BOX_concat",
+ "ttN textDebug",
+ "ttN xyPlot"
+ ],
+ {
+ "author": "tinyterra",
+ "description": "This extension offers various pipe nodes, fullscreen image viewer based on node history, dynamic widgets, interface customization, and more.",
+ "nickname": "ttNodes",
+ "nodename_pattern": "^ttN ",
+ "title": "tinyterraNodes",
+ "title_aux": "tinyterraNodes"
+ }
+ ],
+ "https://github.com/TripleHeadedMonkey/ComfyUI_MileHighStyler": [
+ [
+ "menus"
+ ],
+ {
+ "title_aux": "ComfyUI_MileHighStyler"
+ }
+ ],
+ "https://github.com/Tropfchen/ComfyUI-Embedding_Picker": [
+ [
+ "EmbeddingPicker"
+ ],
+ {
+ "title_aux": "Embedding Picker"
+ }
+ ],
+ "https://github.com/Tropfchen/ComfyUI-yaResolutionSelector": [
+ [
+ "YARS",
+ "YARSAdv"
+ ],
+ {
+ "title_aux": "YARS: Yet Another Resolution Selector"
+ }
+ ],
+ "https://github.com/Trung0246/ComfyUI-0246": [
+ [
+ "0246.Beautify",
+ "0246.BoxRange",
+ "0246.CastReroute",
+ "0246.Convert",
+ "0246.Count",
+ "0246.Highway",
+ "0246.HighwayBatch",
+ "0246.Hold",
+ "0246.Hub",
+ "0246.Junction",
+ "0246.JunctionBatch",
+ "0246.Loop",
+ "0246.Merge",
+ "0246.Pick",
+ "0246.RandomInt",
+ "0246.Script",
+ "0246.ScriptImbue",
+ "0246.ScriptNode",
+ "0246.ScriptPlan",
+ "0246.ScriptRule",
+ "0246.Stringify"
+ ],
+ {
+ "author": "Trung0246",
+ "description": "Random nodes for ComfyUI I made to solve my struggle with ComfyUI (ex: pipe, process). Have varying quality.",
+ "nickname": "ComfyUI-0246",
+ "title": "ComfyUI-0246",
+ "title_aux": "ComfyUI-0246"
+ }
+ ],
+ "https://github.com/Ttl/ComfyUi_NNLatentUpscale": [
+ [
+ "NNLatentUpscale"
+ ],
+ {
+ "title_aux": "ComfyUI Neural network latent upscale custom node"
+ }
+ ],
+ "https://github.com/Umikaze-job/select_folder_path_easy": [
+ [
+ "SelectFolderPathEasy"
+ ],
+ {
+ "title_aux": "select_folder_path_easy"
+ }
+ ],
+ "https://github.com/WASasquatch/ASTERR": [
+ [
+ "ASTERR",
+ "SaveASTERR"
+ ],
+ {
+ "title_aux": "ASTERR"
+ }
+ ],
+ "https://github.com/WASasquatch/ComfyUI_Preset_Merger": [
+ [
+ "Preset_Model_Merge"
+ ],
+ {
+ "title_aux": "ComfyUI Preset Merger"
+ }
+ ],
+ "https://github.com/WASasquatch/FreeU_Advanced": [
+ [
+ "FreeU (Advanced)"
+ ],
+ {
+ "title_aux": "FreeU_Advanced"
+ }
+ ],
+ "https://github.com/WASasquatch/PPF_Noise_ComfyUI": [
+ [
+ "Blend Latents (PPF Noise)",
+ "Cross-Hatch Power Fractal (PPF Noise)",
+ "Images as Latents (PPF Noise)",
+ "Perlin Power Fractal Latent (PPF Noise)"
+ ],
+ {
+ "title_aux": "PPF_Noise_ComfyUI"
+ }
+ ],
+ "https://github.com/WASasquatch/PowerNoiseSuite": [
+ [
+ "Blend Latents (PPF Noise)",
+ "Cross-Hatch Power Fractal (PPF Noise)",
+ "Cross-Hatch Power Fractal Settings (PPF Noise)",
+ "Images as Latents (PPF Noise)",
+ "Latent Adjustment (PPF Noise)",
+ "Latents to CPU (PPF Noise)",
+ "Linear Cross-Hatch Power Fractal (PPF Noise)",
+ "Perlin Power Fractal Latent (PPF Noise)",
+ "Perlin Power Fractal Settings (PPF Noise)",
+ "Power KSampler Advanced (PPF Noise)",
+ "Power-Law Noise (PPF Noise)"
+ ],
+ {
+ "title_aux": "Power Noise Suite for ComfyUI"
+ }
+ ],
+ "https://github.com/WASasquatch/WAS_Extras": [
+ [
+ "BLVAEEncode",
+ "CLIPTextEncodeList",
+ "CLIPTextEncodeSequence2",
+ "ConditioningBlend",
+ "DebugInput",
+ "KSamplerSeq",
+ "KSamplerSeq2",
+ "VAEEncodeForInpaint (WAS)",
+ "VividSharpen"
+ ],
+ {
+ "title_aux": "WAS_Extras"
+ }
+ ],
+ "https://github.com/WASasquatch/was-node-suite-comfyui": [
+ [
+ "BLIP Analyze Image",
+ "BLIP Model Loader",
+ "Blend Latents",
+ "Bounded Image Blend",
+ "Bounded Image Blend with Mask",
+ "Bounded Image Crop",
+ "Bounded Image Crop with Mask",
+ "Bus Node",
+ "CLIP Input Switch",
+ "CLIP Vision Input Switch",
+ "CLIPSeg Batch Masking",
+ "CLIPSeg Masking",
+ "CLIPSeg Model Loader",
+ "CLIPTextEncode (BlenderNeko Advanced + NSP)",
+ "CLIPTextEncode (NSP)",
+ "Cache Node",
+ "Checkpoint Loader",
+ "Checkpoint Loader (Simple)",
+ "Conditioning Input Switch",
+ "Constant Number",
+ "Control Net Model Input Switch",
+ "Convert Masks to Images",
+ "Create Grid Image",
+ "Create Grid Image from Batch",
+ "Create Morph Image",
+ "Create Morph Image from Path",
+ "Create Video from Path",
+ "Debug Number to Console",
+ "Dictionary to Console",
+ "Diffusers Hub Model Down-Loader",
+ "Diffusers Model Loader",
+ "Export API",
+ "Image Analyze",
+ "Image Aspect Ratio",
+ "Image Batch",
+ "Image Blank",
+ "Image Blend",
+ "Image Blend by Mask",
+ "Image Blending Mode",
+ "Image Bloom Filter",
+ "Image Bounds",
+ "Image Bounds to Console",
+ "Image Canny Filter",
+ "Image Chromatic Aberration",
+ "Image Color Palette",
+ "Image Crop Face",
+ "Image Crop Location",
+ "Image Crop Square Location",
+ "Image Displacement Warp",
+ "Image Dragan Photography Filter",
+ "Image Edge Detection Filter",
+ "Image Film Grain",
+ "Image Filter Adjustments",
+ "Image Flip",
+ "Image Generate Gradient",
+ "Image Gradient Map",
+ "Image High Pass Filter",
+ "Image History Loader",
+ "Image Input Switch",
+ "Image Levels Adjustment",
+ "Image Load",
+ "Image Lucy Sharpen",
+ "Image Median Filter",
+ "Image Mix RGB Channels",
+ "Image Monitor Effects Filter",
+ "Image Nova Filter",
+ "Image Padding",
+ "Image Paste Crop",
+ "Image Paste Crop by Location",
+ "Image Paste Face",
+ "Image Perlin Noise",
+ "Image Perlin Power Fractal",
+ "Image Pixelate",
+ "Image Power Noise",
+ "Image Rembg (Remove Background)",
+ "Image Remove Background (Alpha)",
+ "Image Remove Color",
+ "Image Resize",
+ "Image Rotate",
+ "Image Rotate Hue",
+ "Image SSAO (Ambient Occlusion)",
+ "Image SSDO (Direct Occlusion)",
+ "Image Save",
+ "Image Seamless Texture",
+ "Image Select Channel",
+ "Image Select Color",
+ "Image Shadows and Highlights",
+ "Image Size to Number",
+ "Image Stitch",
+ "Image Style Filter",
+ "Image Threshold",
+ "Image Tiled",
+ "Image Transpose",
+ "Image Voronoi Noise Filter",
+ "Image fDOF Filter",
+ "Image to Latent Mask",
+ "Image to Noise",
+ "Image to Seed",
+ "Images to Linear",
+ "Images to RGB",
+ "Inset Image Bounds",
+ "Integer place counter",
+ "KSampler (WAS)",
+ "KSampler Cycle",
+ "Latent Input Switch",
+ "Latent Noise Injection",
+ "Latent Size to Number",
+ "Latent Upscale by Factor (WAS)",
+ "Load Cache",
+ "Load Image Batch",
+ "Load Lora",
+ "Load Text File",
+ "Logic Boolean",
+ "Lora Input Switch",
+ "Lora Loader",
+ "Mask Arbitrary Region",
+ "Mask Batch",
+ "Mask Batch to Mask",
+ "Mask Ceiling Region",
+ "Mask Crop Dominant Region",
+ "Mask Crop Minority Region",
+ "Mask Crop Region",
+ "Mask Dilate Region",
+ "Mask Dominant Region",
+ "Mask Erode Region",
+ "Mask Fill Holes",
+ "Mask Floor Region",
+ "Mask Gaussian Region",
+ "Mask Invert",
+ "Mask Minority Region",
+ "Mask Paste Region",
+ "Mask Smooth Region",
+ "Mask Threshold Region",
+ "Masks Add",
+ "Masks Combine Batch",
+ "Masks Combine Regions",
+ "Masks Subtract",
+ "MiDaS Depth Approximation",
+ "MiDaS Mask Image",
+ "MiDaS Model Loader",
+ "Model Input Switch",
+ "Number Counter",
+ "Number Input Condition",
+ "Number Input Switch",
+ "Number Multiple Of",
+ "Number Operation",
+ "Number PI",
+ "Number to Float",
+ "Number to Int",
+ "Number to Seed",
+ "Number to String",
+ "Number to Text",
+ "Prompt Multiple Styles Selector",
+ "Prompt Styles Selector",
+ "Random Number",
+ "SAM Image Mask",
+ "SAM Model Loader",
+ "SAM Parameters",
+ "SAM Parameters Combine",
+ "Samples Passthrough (Stat System)",
+ "Save Text File",
+ "Seed",
+ "String to Text",
+ "Tensor Batch to Image",
+ "Text Add Token by Input",
+ "Text Add Tokens",
+ "Text Compare",
+ "Text Concatenate",
+ "Text Dictionary Update",
+ "Text File History Loader",
+ "Text Find and Replace",
+ "Text Find and Replace Input",
+ "Text Find and Replace by Dictionary",
+ "Text Input Switch",
+ "Text List",
+ "Text List Concatenate",
+ "Text List to Text",
+ "Text Load Line From File",
+ "Text Multiline",
+ "Text Parse A1111 Embeddings",
+ "Text Parse Noodle Soup Prompts",
+ "Text Parse Tokens",
+ "Text Random Line",
+ "Text Random Prompt",
+ "Text Shuffle",
+ "Text String",
+ "Text String Truncate",
+ "Text to Conditioning",
+ "Text to Console",
+ "Text to Number",
+ "Text to String",
+ "True Random.org Number Generator",
+ "Upscale Model Loader",
+ "Upscale Model Switch",
+ "VAE Input Switch",
+ "Video Dump Frames",
+ "Write to GIF",
+ "Write to Video",
+ "unCLIP Checkpoint Loader"
+ ],
+ {
+ "title_aux": "WAS Node Suite"
+ }
+ ],
+ "https://github.com/WebDev9000/WebDev9000-Nodes": [
+ [
+ "IgnoreBraces",
+ "SettingsSwitch"
+ ],
+ {
+ "title_aux": "WebDev9000-Nodes"
+ }
+ ],
+ "https://github.com/YMC-GitHub/ymc-node-suite-comfyui": [
+ [
+ "canvas-util-cal-size",
+ "conditioning-util-input-switch",
+ "cutoff-region-util",
+ "hks-util-cal-denoise-step",
+ "img-util-get-image-size",
+ "img-util-switch-input-image",
+ "io-image-save",
+ "io-text-save",
+ "io-util-file-list-get",
+ "io-util-file-list-get-text",
+ "number-util-random-num",
+ "pipe-util-to-basic-pipe",
+ "region-util-get-by-center-and-size",
+ "region-util-get-by-lt",
+ "region-util-get-crop-location-from-center-size-text",
+ "region-util-get-pad-out-location-by-size",
+ "text-preset-colors",
+ "text-util-join-text",
+ "text-util-loop-text",
+ "text-util-path-list",
+ "text-util-prompt-add-prompt",
+ "text-util-prompt-adv-dup",
+ "text-util-prompt-adv-search",
+ "text-util-prompt-del",
+ "text-util-prompt-dup",
+ "text-util-prompt-join",
+ "text-util-prompt-search",
+ "text-util-prompt-shuffle",
+ "text-util-prompt-std",
+ "text-util-prompt-unweight",
+ "text-util-random-text",
+ "text-util-search-text",
+ "text-util-show-text",
+ "text-util-switch-text",
+ "xyz-util-txt-to-int"
+ ],
+ {
+ "title_aux": "ymc-node-suite-comfyui"
+ }
+ ],
+ "https://github.com/YOUR-WORST-TACO/ComfyUI-TacoNodes": [
+ [
+ "Example",
+ "TacoAnimatedLoader",
+ "TacoGifMaker",
+ "TacoImg2ImgAnimatedLoader",
+ "TacoImg2ImgAnimatedProcessor",
+ "TacoLatent"
+ ],
+ {
+ "title_aux": "ComfyUI-TacoNodes"
+ }
+ ],
+ "https://github.com/YinBailiang/MergeBlockWeighted_fo_ComfyUI": [
+ [
+ "MergeBlockWeighted"
+ ],
+ {
+ "title_aux": "MergeBlockWeighted_fo_ComfyUI"
+ }
+ ],
+ "https://github.com/ZHO-ZHO-ZHO/ComfyUI-Gemini": [
+ [
+ "ConcatText_Zho",
+ "DisplayText_Zho",
+ "Gemini_API_Chat_Zho",
+ "Gemini_API_S_Chat_Zho",
+ "Gemini_API_S_Vsion_ImgURL_Zho",
+ "Gemini_API_S_Zho",
+ "Gemini_API_Vsion_ImgURL_Zho",
+ "Gemini_API_Zho"
+ ],
+ {
+ "title_aux": "ComfyUI-Gemini"
+ }
+ ],
+ "https://github.com/ZHO-ZHO-ZHO/ComfyUI-Text_Image-Composite": [
+ [
+ "AlphaChanelAddByMask",
+ "ImageCompositeBy_BG_Zho",
+ "ImageCompositeBy_Zho",
+ "ImageComposite_BG_Zho",
+ "ImageComposite_Zho",
+ "RGB_Image_Zho",
+ "Text_Image_Frame_Zho",
+ "Text_Image_Multiline_Zho",
+ "Text_Image_Zho"
+ ],
+ {
+ "title_aux": "ComfyUI-Text_Image-Composite [WIP]"
+ }
+ ],
+ "https://github.com/ZHO-ZHO-ZHO/comfyui-portrait-master-zh-cn": [
+ [
+ "PortraitMaster_\u4e2d\u6587\u7248"
+ ],
+ {
+ "title_aux": "comfyui-portrait-master-zh-cn"
+ }
+ ],
+ "https://github.com/ZaneA/ComfyUI-ImageReward": [
+ [
+ "ImageRewardLoader",
+ "ImageRewardScore"
+ ],
+ {
+ "title_aux": "ImageReward"
+ }
+ ],
+ "https://github.com/Zuellni/ComfyUI-ExLlama": [
+ [
+ "ZuellniExLlamaGenerator",
+ "ZuellniExLlamaLoader",
+ "ZuellniTextPreview",
+ "ZuellniTextReplace"
+ ],
+ {
+ "title_aux": "ComfyUI-ExLlama"
+ }
+ ],
+ "https://github.com/Zuellni/ComfyUI-PickScore-Nodes": [
+ [
+ "ZuellniPickScoreImageProcessor",
+ "ZuellniPickScoreLoader",
+ "ZuellniPickScoreSelector",
+ "ZuellniPickScoreTextProcessor"
+ ],
+ {
+ "title_aux": "ComfyUI PickScore Nodes"
+ }
+ ],
+ "https://github.com/a1lazydog/ComfyUI-AudioScheduler": [
+ [
+ "AmplitudeToGraph",
+ "AmplitudeToNumber",
+ "AudioToAmplitudeGraph",
+ "AudioToFFTs",
+ "BatchAmplitudeSchedule",
+ "ClipAmplitude",
+ "GateNormalizedAmplitude",
+ "LoadAudio",
+ "NormalizeAmplitude",
+ "NormalizedAmplitudeDrivenString",
+ "NormalizedAmplitudeToGraph",
+ "NormalizedAmplitudeToNumber",
+ "TransientAmplitudeBasic"
+ ],
+ {
+ "title_aux": "ComfyUI-AudioScheduler"
+ }
+ ],
+ "https://github.com/adieyal/comfyui-dynamicprompts": [
+ [
+ "DPCombinatorialGenerator",
+ "DPFeelingLucky",
+ "DPJinja",
+ "DPMagicPrompt",
+ "DPOutput",
+ "DPRandomGenerator"
+ ],
+ {
+ "title_aux": "DynamicPrompts Custom Nodes"
+ }
+ ],
+ "https://github.com/aegis72/aegisflow_utility_nodes": [
+ [
+ "Aegisflow Image Pass",
+ "Aegisflow Latent Pass",
+ "Aegisflow Model Pass",
+ "Aegisflow VAE Pass",
+ "Aegisflow controlnet preprocessor bus",
+ "Brightness & Contrast_Ally",
+ "Gaussian Blur_Ally",
+ "Image Flip_ally",
+ "Placeholder Tuple",
+ "aegisflow Multi_Pass"
+ ],
+ {
+ "title_aux": "AegisFlow Utility Nodes"
+ }
+ ],
+ "https://github.com/ai-liam/comfyui_liam_util": [
+ [
+ "LiamLoadImage"
+ ],
+ {
+ "title_aux": "LiamUtil"
+ }
+ ],
+ "https://github.com/aianimation55/ComfyUI-FatLabels": [
+ [
+ "FatLabels"
+ ],
+ {
+ "title_aux": "Comfy UI FatLabels"
+ }
+ ],
+ "https://github.com/alpertunga-bile/prompt-generator-comfyui": [
+ [
+ "Prompt Generator"
+ ],
+ {
+ "title_aux": "prompt-generator"
+ }
+ ],
+ "https://github.com/alsritter/asymmetric-tiling-comfyui": [
+ [
+ "Asymmetric_Tiling_KSampler"
+ ],
+ {
+ "title_aux": "asymmetric-tiling-comfyui"
+ }
+ ],
+ "https://github.com/alt-key-project/comfyui-dream-project": [
+ [
+ "Analyze Palette [Dream]",
+ "Beat Curve [Dream]",
+ "Big Float Switch [Dream]",
+ "Big Image Switch [Dream]",
+ "Big Int Switch [Dream]",
+ "Big Latent Switch [Dream]",
+ "Big Palette Switch [Dream]",
+ "Big Text Switch [Dream]",
+ "Boolean To Float [Dream]",
+ "Boolean To Int [Dream]",
+ "Build Prompt [Dream]",
+ "CSV Curve [Dream]",
+ "CSV Generator [Dream]",
+ "Calculation [Dream]",
+ "Common Frame Dimensions [Dream]",
+ "Compare Palettes [Dream]",
+ "FFMPEG Video Encoder [Dream]",
+ "File Count [Dream]",
+ "Finalize Prompt [Dream]",
+ "Float Input [Dream]",
+ "Float to Log Entry [Dream]",
+ "Frame Count Calculator [Dream]",
+ "Frame Counter (Directory) [Dream]",
+ "Frame Counter (Simple) [Dream]",
+ "Frame Counter Info [Dream]",
+ "Frame Counter Offset [Dream]",
+ "Frame Counter Time Offset [Dream]",
+ "Image Brightness Adjustment [Dream]",
+ "Image Color Shift [Dream]",
+ "Image Contrast Adjustment [Dream]",
+ "Image Motion [Dream]",
+ "Image Sequence Blend [Dream]",
+ "Image Sequence Loader [Dream]",
+ "Image Sequence Saver [Dream]",
+ "Image Sequence Tweening [Dream]",
+ "Int Input [Dream]",
+ "Int to Log Entry [Dream]",
+ "Laboratory [Dream]",
+ "Linear Curve [Dream]",
+ "Log Entry Joiner [Dream]",
+ "Log File [Dream]",
+ "Noise from Area Palettes [Dream]",
+ "Noise from Palette [Dream]",
+ "Palette Color Align [Dream]",
+ "Palette Color Shift [Dream]",
+ "Sample Image Area as Palette [Dream]",
+ "Sample Image as Palette [Dream]",
+ "Saw Curve [Dream]",
+ "Sine Curve [Dream]",
+ "Smooth Event Curve [Dream]",
+ "String Input [Dream]",
+ "String Tokenizer [Dream]",
+ "String to Log Entry [Dream]",
+ "Text Input [Dream]",
+ "Triangle Curve [Dream]",
+ "Triangle Event Curve [Dream]",
+ "WAV Curve [Dream]"
+ ],
+ {
+ "title_aux": "Dream Project Animation Nodes"
+ }
+ ],
+ "https://github.com/alt-key-project/comfyui-dream-video-batches": [
+ [
+ "Blended Transition [DVB]",
+ "Calculation [DVB]",
+ "Create Frame Set [DVB]",
+ "Divide [DVB]",
+ "Fade From Black [DVB]",
+ "Fade To Black [DVB]",
+ "Float Input [DVB]",
+ "For Each Done [DVB]",
+ "For Each Filename [DVB]",
+ "Frame Set Append [DVB]",
+ "Frame Set Frame Dimensions Scaled [DVB]",
+ "Frame Set Index Offset [DVB]",
+ "Frame Set Merger [DVB]",
+ "Frame Set Reindex [DVB]",
+ "Frame Set Repeat [DVB]",
+ "Frame Set Reverse [DVB]",
+ "Frame Set Split Beginning [DVB]",
+ "Frame Set Split End [DVB]",
+ "Frame Set Splitter [DVB]",
+ "Generate Inbetween Frames [DVB]",
+ "Int Input [DVB]",
+ "Linear Camera Pan [DVB]",
+ "Linear Camera Roll [DVB]",
+ "Linear Camera Zoom [DVB]",
+ "Load Image From Path [DVB]",
+ "Multiply [DVB]",
+ "Sine Camera Pan [DVB]",
+ "Sine Camera Roll [DVB]",
+ "Sine Camera Zoom [DVB]",
+ "String Input [DVB]",
+ "Text Input [DVB]",
+ "Trace Memory Allocation [DVB]",
+ "Unwrap Frame Set [DVB]"
+ ],
+ {
+ "title_aux": "Dream Video Batches"
+ }
+ ],
+ "https://github.com/an90ray/ComfyUI_RErouter_CustomNodes": [
+ [
+ "CLIPTextEncode (RE)",
+ "CLIPTextEncodeSDXL (RE)",
+ "CLIPTextEncodeSDXLRefiner (RE)",
+ "Int (RE)",
+ "RErouter <=",
+ "RErouter =>",
+ "String (RE)"
+ ],
+ {
+ "title_aux": "ComfyUI-DareMerge"
+ }
+ ],
+ "https://github.com/andersxa/comfyui-PromptAttention": [
+ [
+ "CLIPAttentionMaskEncode"
+ ],
+ {
+ "title_aux": "CLIP Directional Prompt Attention"
+ }
+ ],
+ "https://github.com/asagi4/ComfyUI-CADS": [
+ [
+ "CADS"
+ ],
+ {
+ "title_aux": "ComfyUI-CADS"
+ }
+ ],
+ "https://github.com/asagi4/comfyui-prompt-control": [
+ [
+ "EditableCLIPEncode",
+ "FilterSchedule",
+ "LoRAScheduler",
+ "PCSplitSampling",
+ "PromptControlSimple",
+ "PromptToSchedule",
+ "ScheduleToCond",
+ "ScheduleToModel"
+ ],
+ {
+ "title_aux": "ComfyUI prompt control"
+ }
+ ],
+ "https://github.com/asagi4/comfyui-utility-nodes": [
+ [
+ "MUJinjaRender",
+ "MUSimpleWildcard"
+ ],
+ {
+ "title_aux": "asagi4/comfyui-utility-nodes"
+ }
+ ],
+ "https://github.com/aszc-dev/ComfyUI-CoreMLSuite": [
+ [
+ "Core ML Converter",
+ "Core ML LCM Converter",
+ "Core ML LoRA Loader",
+ "CoreMLModelAdapter",
+ "CoreMLSampler",
+ "CoreMLSamplerAdvanced",
+ "CoreMLUNetLoader"
+ ],
+ {
+ "title_aux": "Core ML Suite for ComfyUI"
+ }
+ ],
+ "https://github.com/avatechai/avatar-graph-comfyui": [
+ [
+ "ApplyMeshTransformAsShapeKey",
+ "B_ENUM",
+ "B_VECTOR3",
+ "B_VECTOR4",
+ "Combine Points",
+ "CreateShapeFlow",
+ "ExportBlendshapes",
+ "ExportGLTF",
+ "Extract Boundary Points",
+ "Image Alpha Mask Merge",
+ "ImageBridge",
+ "LoadImageFromRequest",
+ "LoadImageWithAlpha",
+ "SAM MultiLayer",
+ "Save Image With Workflow"
+ ],
+ {
+ "author": "Avatech Limited",
+ "description": "Include nodes for sam + bpy operation, that allows workflow creations for generative 2d character rig.",
+ "nickname": "Avatar Graph",
+ "title": "Avatar Graph",
+ "title_aux": "avatar-graph-comfyui"
+ }
+ ],
+ "https://github.com/azazeal04/ComfyUI-Styles": [
+ [
+ "menus"
+ ],
+ {
+ "title_aux": "ComfyUI-Styles"
+ }
+ ],
+ "https://github.com/badjeff/comfyui_lora_tag_loader": [
+ [
+ "LoraTagLoader"
+ ],
+ {
+ "title_aux": "LoRA Tag Loader for ComfyUI"
+ }
+ ],
+ "https://github.com/banodoco/steerable-motion": [
+ [
+ "BatchCreativeInterpolation"
+ ],
+ {
+ "title_aux": "Steerable Motion"
+ }
+ ],
+ "https://github.com/bash-j/mikey_nodes": [
+ [
+ "AddMetaData",
+ "Batch Crop Image",
+ "Batch Crop Resize Inplace",
+ "Batch Load Images",
+ "Batch Resize Image for SDXL",
+ "Checkpoint Loader Simple Mikey",
+ "CinematicLook",
+ "Empty Latent Ratio Custom SDXL",
+ "Empty Latent Ratio Select SDXL",
+ "EvalFloats",
+ "FileNamePrefix",
+ "FileNamePrefixDateDirFirst",
+ "Float to String",
+ "HaldCLUT",
+ "Image Caption",
+ "ImageBorder",
+ "ImageOverlay",
+ "ImagePaste",
+ "Int to String",
+ "LMStudioPrompt",
+ "Load Image Based on Number",
+ "LoraSyntaxProcessor",
+ "Mikey Sampler",
+ "Mikey Sampler Base Only",
+ "Mikey Sampler Base Only Advanced",
+ "Mikey Sampler Tiled",
+ "Mikey Sampler Tiled Base Only",
+ "MikeySamplerTiledAdvanced",
+ "MikeySamplerTiledAdvancedBaseOnly",
+ "OobaPrompt",
+ "PresetRatioSelector",
+ "Prompt With SDXL",
+ "Prompt With Style",
+ "Prompt With Style V2",
+ "Prompt With Style V3",
+ "Range Float",
+ "Range Integer",
+ "Ratio Advanced",
+ "Resize Image for SDXL",
+ "Save Image If True",
+ "Save Image With Prompt Data",
+ "Save Images Mikey",
+ "Save Images No Display",
+ "SaveMetaData",
+ "SearchAndReplace",
+ "Seed String",
+ "Style Conditioner",
+ "Style Conditioner Base Only",
+ "Text2InputOr3rdOption",
+ "TextCombinations",
+ "TextCombinations3",
+ "TextConcat",
+ "TextPreserve",
+ "Upscale Tile Calculator",
+ "Wildcard Processor",
+ "WildcardAndLoraSyntaxProcessor",
+ "WildcardOobaPrompt"
+ ],
+ {
+ "title_aux": "Mikey Nodes"
+ }
+ ],
+ "https://github.com/bedovyy/ComfyUI_NAIDGenerator": [
+ [
+ "GenerateNAID",
+ "Img2ImgOptionNAID",
+ "InpaintingOptionNAID",
+ "MaskImageToNAID",
+ "ModelOptionNAID",
+ "PromptToNAID"
+ ],
+ {
+ "title_aux": "ComfyUI_NAIDGenerator"
+ }
+ ],
+ "https://github.com/biegert/ComfyUI-CLIPSeg/raw/main/custom_nodes/clipseg.py": [
+ [
+ "CLIPSeg",
+ "CombineSegMasks"
+ ],
+ {
+ "title_aux": "CLIPSeg"
+ }
+ ],
+ "https://github.com/bmad4ever/comfyui_ab_samplercustom": [
+ [
+ "AB SamplerCustom (experimental)"
+ ],
+ {
+ "title_aux": "comfyui_ab_sampler"
+ }
+ ],
+ "https://github.com/bmad4ever/comfyui_bmad_nodes": [
+ [
+ "AdaptiveThresholding",
+ "Add String To Many",
+ "AddAlpha",
+ "AdjustRect",
+ "AnyToAny",
+ "BoundingRect (contours)",
+ "BuildColorRangeAdvanced (hsv)",
+ "BuildColorRangeHSV (hsv)",
+ "CLAHE",
+ "CLIPEncodeMultiple",
+ "CLIPEncodeMultipleAdvanced",
+ "ChameleonMask",
+ "CheckpointLoader (dirty)",
+ "CheckpointLoaderSimple (dirty)",
+ "Color (RGB)",
+ "Color (hexadecimal)",
+ "Color Clip",
+ "Color Clip (advanced)",
+ "Color Clip ADE20k",
+ "ColorDictionary",
+ "ColorDictionary (custom)",
+ "Conditioning (combine multiple)",
+ "Conditioning (combine selective)",
+ "Conditioning Grid (cond)",
+ "Conditioning Grid (string)",
+ "Conditioning Grid (string) Advanced",
+ "Contour To Mask",
+ "Contours",
+ "ControlNetHadamard",
+ "ControlNetHadamard (manual)",
+ "ConvertImg",
+ "CopyMakeBorder",
+ "CreateRequestMetadata",
+ "DistanceTransform",
+ "Draw Contour(s)",
+ "EqualizeHistogram",
+ "ExtendColorList",
+ "ExtendCondList",
+ "ExtendFloatList",
+ "ExtendImageList",
+ "ExtendIntList",
+ "ExtendLatentList",
+ "ExtendMaskList",
+ "ExtendModelList",
+ "ExtendStringList",
+ "FadeMaskEdges",
+ "Filter Contour",
+ "FindComplementaryColor",
+ "FindThreshold",
+ "FlatLatentsIntoSingleGrid",
+ "Framed Mask Grab Cut",
+ "Framed Mask Grab Cut 2",
+ "FromListGet1Color",
+ "FromListGet1Cond",
+ "FromListGet1Float",
+ "FromListGet1Image",
+ "FromListGet1Int",
+ "FromListGet1Latent",
+ "FromListGet1Mask",
+ "FromListGet1Model",
+ "FromListGet1String",
+ "FromListGetColors",
+ "FromListGetConds",
+ "FromListGetFloats",
+ "FromListGetImages",
+ "FromListGetInts",
+ "FromListGetLatents",
+ "FromListGetMasks",
+ "FromListGetModels",
+ "FromListGetStrings",
+ "Get Contour from list",
+ "Get Models",
+ "Get Prompt",
+ "HypernetworkLoader (dirty)",
+ "ImageBatchToList",
+ "InRange (hsv)",
+ "InnerCylinder (remap)",
+ "Inpaint",
+ "Input/String to Int Array",
+ "KMeansColor",
+ "Load 64 Encoded Image",
+ "LoraLoader (dirty)",
+ "MaskGrid N KSamplers Advanced",
+ "MaskOuterBlur",
+ "Merge Latent Batch Gridwise",
+ "MonoMerge",
+ "MorphologicOperation",
+ "MorphologicSkeletoning",
+ "NaiveAutoKMeansColor",
+ "OtsuThreshold",
+ "OuterCylinder (remap)",
+ "RGB to HSV",
+ "Rect Grab Cut",
+ "Remap",
+ "RemapInsideParabolas (remap)",
+ "RemapInsideParabolasAdvanced (remap)",
+ "RemapQuadrilateral (remap)",
+ "Repeat Into Grid (image)",
+ "Repeat Into Grid (latent)",
+ "RequestInputs",
+ "SampleColorHSV",
+ "Save Image (api)",
+ "SeamlessClone",
+ "SeamlessClone (simple)",
+ "SetRequestStateToComplete",
+ "String",
+ "String to Float",
+ "String to Integer",
+ "ToColorList",
+ "ToCondList",
+ "ToFloatList",
+ "ToImageList",
+ "ToIntList",
+ "ToLatentList",
+ "ToMaskList",
+ "ToModelList",
+ "ToStringList",
+ "UnGridify (image)",
+ "VAEEncodeBatch"
+ ],
+ {
+ "title_aux": "Bmad Nodes"
+ }
+ ],
+ "https://github.com/bmad4ever/comfyui_lists_cartesian_product": [
+ [
+ "AnyListCartesianProduct"
+ ],
+ {
+ "title_aux": "Lists Cartesian Product"
+ }
+ ],
+ "https://github.com/bradsec/ComfyUI_ResolutionSelector": [
+ [
+ "ResolutionSelector"
+ ],
+ {
+ "title_aux": "ResolutionSelector for ComfyUI"
+ }
+ ],
+ "https://github.com/braintacles/braintacles-comfyui-nodes": [
+ [
+ "CLIPTextEncodeSDXL-Multi-IO",
+ "CLIPTextEncodeSDXL-Pipe",
+ "Empty Latent Image from Aspect-Ratio",
+ "Random Find and Replace",
+ "VAE Decode Pipe",
+ "VAE Decode Tiled Pipe",
+ "VAE Encode Pipe",
+ "VAE Encode Tiled Pipe"
+ ],
+ {
+ "title_aux": "braintacles-nodes"
+ }
+ ],
+ "https://github.com/brianfitzgerald/style_aligned_comfy": [
+ [
+ "StyleAlignedBatchAlign",
+ "StyleAlignedReferenceSampler"
+ ],
+ {
+ "title_aux": "StyleAligned for ComfyUI"
+ }
+ ],
+ "https://github.com/bronkula/comfyui-fitsize": [
+ [
+ "FS: Crop Image Into Even Pieces",
+ "FS: Fit Image And Resize",
+ "FS: Fit Size From Image",
+ "FS: Fit Size From Int",
+ "FS: Image Region To Mask",
+ "FS: Load Image And Resize To Fit",
+ "FS: Pick Image From Batch",
+ "FS: Pick Image From Batches",
+ "FS: Pick Image From List"
+ ],
+ {
+ "title_aux": "comfyui-fitsize"
+ }
+ ],
+ "https://github.com/bruefire/ComfyUI-SeqImageLoader": [
+ [
+ "VFrame Loader With Mask Editor",
+ "Video Loader With Mask Editor"
+ ],
+ {
+ "title_aux": "ComfyUI Sequential Image Loader"
+ }
+ ],
+ "https://github.com/budihartono/comfyui_otonx_nodes": [
+ [
+ "OTX Integer Multiple Inputs 4",
+ "OTX Integer Multiple Inputs 5",
+ "OTX Integer Multiple Inputs 6",
+ "OTX KSampler Feeder",
+ "OTX Versatile Multiple Inputs 4",
+ "OTX Versatile Multiple Inputs 5",
+ "OTX Versatile Multiple Inputs 6"
+ ],
+ {
+ "title_aux": "Otonx's Custom Nodes"
+ }
+ ],
+ "https://github.com/bvhari/ComfyUI_ImageProcessing": [
+ [
+ "BilateralFilter",
+ "Brightness",
+ "Gamma",
+ "Hue",
+ "Saturation",
+ "SigmoidCorrection",
+ "UnsharpMask"
+ ],
+ {
+ "title_aux": "ImageProcessing"
+ }
+ ],
+ "https://github.com/bvhari/ComfyUI_LatentToRGB": [
+ [
+ "LatentToRGB"
+ ],
+ {
+ "title_aux": "LatentToRGB"
+ }
+ ],
+ "https://github.com/bvhari/ComfyUI_PerpWeight": [
+ [
+ "CLIPTextEncodePerpWeight"
+ ],
+ {
+ "title_aux": "ComfyUI_PerpWeight"
+ }
+ ],
+ "https://github.com/catscandrive/comfyui-imagesubfolders/raw/main/loadImageWithSubfolders.py": [
+ [
+ "LoadImagewithSubfolders"
+ ],
+ {
+ "title_aux": "Image loader with subfolders"
+ }
+ ],
+ "https://github.com/ceruleandeep/ComfyUI-LLaVA-Captioner": [
+ [
+ "LlavaCaptioner"
+ ],
+ {
+ "title_aux": "ComfyUI LLaVA Captioner"
+ }
+ ],
+ "https://github.com/chflame163/ComfyUI_MSSpeech_TTS": [
+ [
+ "Input Trigger",
+ "MicrosoftSpeech_TTS",
+ "Play Sound",
+ "Play Sound (loop)"
+ ],
+ {
+ "title_aux": "ComfyUI_MSSpeech_TTS"
+ }
+ ],
+ "https://github.com/chibiace/ComfyUI-Chibi-Nodes": [
+ [
+ "ConditionText",
+ "ConditionTextMulti",
+ "ImageAddText",
+ "ImageSimpleResize",
+ "ImageSizeInfo",
+ "ImageTool",
+ "Int2String",
+ "LoadEmbedding",
+ "LoadImageExtended",
+ "Loader",
+ "Prompts",
+ "RandomResolutionLatent",
+ "SaveImages",
+ "SeedGenerator",
+ "SimpleSampler",
+ "TextSplit",
+ "Textbox",
+ "Wildcards"
+ ],
+ {
+ "title_aux": "ComfyUI-Chibi-Nodes"
+ }
+ ],
+ "https://github.com/chrisgoringe/cg-image-picker": [
+ [
+ "Preview Chooser",
+ "Preview Chooser Fabric"
+ ],
+ {
+ "author": "chrisgoringe",
+ "description": "Custom nodes that preview images and pause the workflow to allow the user to select one or more to progress",
+ "nickname": "Image Chooser",
+ "title": "Image Chooser",
+ "title_aux": "Image chooser"
+ }
+ ],
+ "https://github.com/chrisgoringe/cg-noise": [
+ [
+ "Hijack",
+ "KSampler Advanced with Variations",
+ "KSampler with Variations",
+ "UnHijack"
+ ],
+ {
+ "title_aux": "Variation seeds"
+ }
+ ],
+ "https://github.com/chrisgoringe/cg-use-everywhere": [
+ [
+ "Seed Everywhere"
+ ],
+ {
+ "nodename_pattern": "(^(Prompts|Anything) Everywhere|Simple String)",
+ "title_aux": "Use Everywhere (UE Nodes)"
+ }
+ ],
+ "https://github.com/city96/ComfyUI_ColorMod": [
+ [
+ "ColorModEdges",
+ "ColorModPivot",
+ "LoadImageHighPrec",
+ "PreviewImageHighPrec",
+ "SaveImageHighPrec"
+ ],
+ {
+ "title_aux": "ComfyUI_ColorMod"
+ }
+ ],
+ "https://github.com/city96/ComfyUI_DiT": [
+ [
+ "DiTCheckpointLoader",
+ "DiTCheckpointLoaderSimple",
+ "DiTLabelCombine",
+ "DiTLabelSelect",
+ "DiTSampler"
+ ],
+ {
+ "title_aux": "ComfyUI_DiT [WIP]"
+ }
+ ],
+ "https://github.com/city96/ComfyUI_ExtraModels": [
+ [
+ "DiTCondLabelEmpty",
+ "DiTCondLabelSelect",
+ "DitCheckpointLoader",
+ "ExtraVAELoader",
+ "PixArtCheckpointLoader",
+ "PixArtDPMSampler",
+ "PixArtLoraLoader",
+ "PixArtResolutionSelect",
+ "PixArtT5TextEncode",
+ "T5TextEncode",
+ "T5v11Loader"
+ ],
+ {
+ "title_aux": "Extra Models for ComfyUI"
+ }
+ ],
+ "https://github.com/city96/ComfyUI_NetDist": [
+ [
+ "FetchRemote",
+ "QueueRemote"
+ ],
+ {
+ "title_aux": "ComfyUI_NetDist"
+ }
+ ],
+ "https://github.com/city96/SD-Advanced-Noise": [
+ [
+ "LatentGaussianNoise",
+ "MathEncode"
+ ],
+ {
+ "title_aux": "SD-Advanced-Noise"
+ }
+ ],
+ "https://github.com/city96/SD-Latent-Interposer": [
+ [
+ "LatentInterposer"
+ ],
+ {
+ "title_aux": "Latent-Interposer"
+ }
+ ],
+ "https://github.com/city96/SD-Latent-Upscaler": [
+ [
+ "LatentUpscaler"
+ ],
+ {
+ "title_aux": "SD-Latent-Upscaler"
+ }
+ ],
+ "https://github.com/civitai/comfy-nodes": [
+ [
+ "CivitAI_Checkpoint_Loader",
+ "CivitAI_Lora_Loader"
+ ],
+ {
+ "title_aux": "comfy-nodes"
+ }
+ ],
+ "https://github.com/comfyanonymous/ComfyUI": [
+ [
+ "BasicScheduler",
+ "CLIPLoader",
+ "CLIPMergeSimple",
+ "CLIPSave",
+ "CLIPSetLastLayer",
+ "CLIPTextEncode",
+ "CLIPTextEncodeSDXL",
+ "CLIPTextEncodeSDXLRefiner",
+ "CLIPVisionEncode",
+ "CLIPVisionLoader",
+ "Canny",
+ "CheckpointLoader",
+ "CheckpointLoaderSimple",
+ "CheckpointSave",
+ "ConditioningAverage",
+ "ConditioningCombine",
+ "ConditioningConcat",
+ "ConditioningSetArea",
+ "ConditioningSetAreaPercentage",
+ "ConditioningSetMask",
+ "ConditioningSetTimestepRange",
+ "ConditioningZeroOut",
+ "ControlNetApply",
+ "ControlNetApplyAdvanced",
+ "ControlNetLoader",
+ "CropMask",
+ "DiffControlNetLoader",
+ "DiffusersLoader",
+ "DualCLIPLoader",
+ "EmptyImage",
+ "EmptyLatentImage",
+ "ExponentialScheduler",
+ "FeatherMask",
+ "FlipSigmas",
+ "FreeU",
+ "FreeU_V2",
+ "GLIGENLoader",
+ "GLIGENTextBoxApply",
+ "GrowMask",
+ "HyperTile",
+ "HypernetworkLoader",
+ "ImageBatch",
+ "ImageBlend",
+ "ImageBlur",
+ "ImageColorToMask",
+ "ImageCompositeMasked",
+ "ImageCrop",
+ "ImageInvert",
+ "ImageOnlyCheckpointLoader",
+ "ImagePadForOutpaint",
+ "ImageQuantize",
+ "ImageScale",
+ "ImageScaleBy",
+ "ImageScaleToTotalPixels",
+ "ImageSharpen",
+ "ImageToMask",
+ "ImageUpscaleWithModel",
+ "InvertMask",
+ "JoinImageWithAlpha",
+ "KSampler",
+ "KSamplerAdvanced",
+ "KSamplerSelect",
+ "KarrasScheduler",
+ "LatentAdd",
+ "LatentBatch",
+ "LatentBlend",
+ "LatentComposite",
+ "LatentCompositeMasked",
+ "LatentCrop",
+ "LatentFlip",
+ "LatentFromBatch",
+ "LatentInterpolate",
+ "LatentMultiply",
+ "LatentRotate",
+ "LatentSubtract",
+ "LatentUpscale",
+ "LatentUpscaleBy",
+ "LoadImage",
+ "LoadImageMask",
+ "LoadLatent",
+ "LoraLoader",
+ "LoraLoaderModelOnly",
+ "MaskComposite",
+ "MaskToImage",
+ "ModelMergeAdd",
+ "ModelMergeBlocks",
+ "ModelMergeSimple",
+ "ModelMergeSubtract",
+ "ModelSamplingContinuousEDM",
+ "ModelSamplingDiscrete",
+ "PatchModelAddDownscale",
+ "PerpNeg",
+ "PolyexponentialScheduler",
+ "PorterDuffImageComposite",
+ "PreviewImage",
+ "RebatchImages",
+ "RebatchLatents",
+ "RepeatImageBatch",
+ "RepeatLatentBatch",
+ "RescaleCFG",
+ "SDTurboScheduler",
+ "SVD_img2vid_Conditioning",
+ "SamplerCustom",
+ "SamplerDPMPP_2M_SDE",
+ "SamplerDPMPP_SDE",
+ "SaveAnimatedPNG",
+ "SaveAnimatedWEBP",
+ "SaveImage",
+ "SaveLatent",
+ "SelfAttentionGuidance",
+ "SetLatentNoiseMask",
+ "SolidMask",
+ "SplitImageWithAlpha",
+ "SplitSigmas",
+ "StableZero123_Conditioning",
+ "StyleModelApply",
+ "StyleModelLoader",
+ "TomePatchModel",
+ "UNETLoader",
+ "UpscaleModelLoader",
+ "VAEDecode",
+ "VAEDecodeTiled",
+ "VAEEncode",
+ "VAEEncodeForInpaint",
+ "VAEEncodeTiled",
+ "VAELoader",
+ "VAESave",
+ "VPScheduler",
+ "VideoLinearCFGGuidance",
+ "unCLIPCheckpointLoader",
+ "unCLIPConditioning"
+ ],
+ {
+ "title_aux": "ComfyUI"
+ }
+ ],
+ "https://github.com/comfyanonymous/ComfyUI_experiments": [
+ [
+ "ModelMergeBlockNumber",
+ "ModelMergeSDXL",
+ "ModelMergeSDXLDetailedTransformers",
+ "ModelMergeSDXLTransformers",
+ "ModelSamplerTonemapNoiseTest",
+ "ReferenceOnlySimple",
+ "RescaleClassifierFreeGuidanceTest",
+ "TonemapNoiseWithRescaleCFG"
+ ],
+ {
+ "title_aux": "ComfyUI_experiments"
+ }
+ ],
+ "https://github.com/concarne000/ConCarneNode": [
+ [
+ "BingImageGrabber",
+ "Zephyr"
+ ],
+ {
+ "title_aux": "ConCarneNode"
+ }
+ ],
+ "https://github.com/coreyryanhanson/ComfyQR": [
+ [
+ "comfy-qr-by-image-size",
+ "comfy-qr-by-module-size",
+ "comfy-qr-by-module-split",
+ "comfy-qr-mask_errors"
+ ],
+ {
+ "title_aux": "ComfyQR"
+ }
+ ],
+ "https://github.com/coreyryanhanson/ComfyQR-scanning-nodes": [
+ [
+ "comfy-qr-read",
+ "comfy-qr-validate"
+ ],
+ {
+ "title_aux": "ComfyQR-scanning-nodes"
+ }
+ ],
+ "https://github.com/cubiq/ComfyUI_IPAdapter_plus": [
+ [
+ "IPAdapterApply",
+ "IPAdapterApplyEncoded",
+ "IPAdapterBatchEmbeds",
+ "IPAdapterEncoder",
+ "IPAdapterLoadEmbeds",
+ "IPAdapterModelLoader",
+ "IPAdapterSaveEmbeds",
+ "InsightFaceLoader",
+ "PrepImageForClipVision",
+ "PrepImageForInsightFace"
+ ],
+ {
+ "title_aux": "ComfyUI_IPAdapter_plus"
+ }
+ ],
+ "https://github.com/cubiq/ComfyUI_SimpleMath": [
+ [
+ "SimpleMath",
+ "SimpleMathDebug"
+ ],
+ {
+ "title_aux": "Simple Math"
+ }
+ ],
+ "https://github.com/cubiq/ComfyUI_essentials": [
+ [
+ "ConsoleDebug+",
+ "ExtractKeyframes+",
+ "GetImageSize+",
+ "ImageCASharpening+",
+ "ImageCrop+",
+ "ImageDesaturate+",
+ "ImageEnhanceDifference+",
+ "ImageExpandBatch+",
+ "ImageFlip+",
+ "ImageFromBatch+",
+ "ImagePosterize+",
+ "ImageResize+",
+ "MaskBatch+",
+ "MaskBlur+",
+ "MaskExpandBatch+",
+ "MaskFlip+",
+ "MaskFromBatch+",
+ "MaskFromColor+",
+ "MaskPreview+",
+ "ModelCompile+",
+ "SimpleMath+",
+ "StableZero123_Increments",
+ "TransitionMask+"
+ ],
+ {
+ "title_aux": "ComfyUI Essentials"
+ }
+ ],
+ "https://github.com/dagthomas/comfyui_dagthomas": [
+ [
+ "CSL",
+ "CSVPromptGenerator",
+ "PromptGenerator"
+ ],
+ {
+ "title_aux": "SDXL Auto Prompter"
+ }
+ ],
+ "https://github.com/dawangraoming/ComfyUI_ksampler_gpu/raw/main/ksampler_gpu.py": [
+ [
+ "KSamplerAdvancedGPU",
+ "KSamplerGPU"
+ ],
+ {
+ "title_aux": "KSampler GPU"
+ }
+ ],
+ "https://github.com/daxthin/DZ-FaceDetailer": [
+ [
+ "DZ_Face_Detailer"
+ ],
+ {
+ "title_aux": "DZ-FaceDetailer"
+ }
+ ],
+ "https://github.com/deroberon/StableZero123-comfyui": [
+ [
+ "SDZero ImageSplit",
+ "Stablezero123",
+ "Stablezero123WithDepth"
+ ],
+ {
+ "title_aux": "StableZero123-comfyui"
+ }
+ ],
+ "https://github.com/deroberon/demofusion-comfyui": [
+ [
+ "Batch Unsampler",
+ "Demofusion",
+ "Demofusion From Single File",
+ "Iterative Mixing KSampler"
+ ],
+ {
+ "title_aux": "demofusion-comfyui"
+ }
+ ],
+ "https://github.com/dimtoneff/ComfyUI-PixelArt-Detector": [
+ [
+ "PixelArtAddDitherPattern",
+ "PixelArtDetectorConverter",
+ "PixelArtDetectorSave",
+ "PixelArtDetectorToImage",
+ "PixelArtLoadPalettes"
+ ],
+ {
+ "title_aux": "ComfyUI PixelArt Detector"
+ }
+ ],
+ "https://github.com/diontimmer/ComfyUI-Vextra-Nodes": [
+ [
+ "Add Text To Image",
+ "Apply Instagram Filter",
+ "Create Solid Color",
+ "Flatten Colors",
+ "Generate Noise Image",
+ "GlitchThis Effect",
+ "Hue Rotation",
+ "Load Picture Index",
+ "Pixel Sort",
+ "Play Sound At Execution",
+ "Prettify Prompt Using distilgpt2",
+ "Swap Color Mode"
+ ],
+ {
+ "title_aux": "ComfyUI-Vextra-Nodes"
+ }
+ ],
+ "https://github.com/dmarx/ComfyUI-Keyframed": [
+ [
+ "Example",
+ "KfAddCurveToPGroup",
+ "KfAddCurveToPGroupx10",
+ "KfApplyCurveToCond",
+ "KfConditioningAdd",
+ "KfConditioningAddx10",
+ "KfCurveConstant",
+ "KfCurveDraw",
+ "KfCurveFromString",
+ "KfCurveFromYAML",
+ "KfCurveInverse",
+ "KfCurveToAcnLatentKeyframe",
+ "KfCurvesAdd",
+ "KfCurvesAddx10",
+ "KfCurvesDivide",
+ "KfCurvesMultiply",
+ "KfCurvesMultiplyx10",
+ "KfCurvesSubtract",
+ "KfDebug_Clip",
+ "KfDebug_Cond",
+ "KfDebug_Curve",
+ "KfDebug_Float",
+ "KfDebug_Image",
+ "KfDebug_Int",
+ "KfDebug_Latent",
+ "KfDebug_Model",
+ "KfDebug_Passthrough",
+ "KfDebug_Segs",
+ "KfDebug_String",
+ "KfDebug_Vae",
+ "KfDrawSchedule",
+ "KfEvaluateCurveAtT",
+ "KfGetCurveFromPGroup",
+ "KfGetScheduleConditionAtTime",
+ "KfGetScheduleConditionSlice",
+ "KfKeyframedCondition",
+ "KfKeyframedConditionWithText",
+ "KfPGroupCurveAdd",
+ "KfPGroupCurveMultiply",
+ "KfPGroupDraw",
+ "KfPGroupProd",
+ "KfPGroupSum",
+ "KfSetCurveLabel",
+ "KfSetKeyframe",
+ "KfSinusoidalAdjustAmplitude",
+ "KfSinusoidalAdjustFrequency",
+ "KfSinusoidalAdjustPhase",
+ "KfSinusoidalAdjustWavelength",
+ "KfSinusoidalEntangledZeroOneFromFrequencyx2",
+ "KfSinusoidalEntangledZeroOneFromFrequencyx3",
+ "KfSinusoidalEntangledZeroOneFromFrequencyx4",
+ "KfSinusoidalEntangledZeroOneFromFrequencyx5",
+ "KfSinusoidalEntangledZeroOneFromFrequencyx6",
+ "KfSinusoidalEntangledZeroOneFromFrequencyx7",
+ "KfSinusoidalEntangledZeroOneFromFrequencyx8",
+ "KfSinusoidalEntangledZeroOneFromFrequencyx9",
+ "KfSinusoidalEntangledZeroOneFromWavelengthx2",
+ "KfSinusoidalEntangledZeroOneFromWavelengthx3",
+ "KfSinusoidalEntangledZeroOneFromWavelengthx4",
+ "KfSinusoidalEntangledZeroOneFromWavelengthx5",
+ "KfSinusoidalEntangledZeroOneFromWavelengthx6",
+ "KfSinusoidalEntangledZeroOneFromWavelengthx7",
+ "KfSinusoidalEntangledZeroOneFromWavelengthx8",
+ "KfSinusoidalEntangledZeroOneFromWavelengthx9",
+ "KfSinusoidalGetAmplitude",
+ "KfSinusoidalGetFrequency",
+ "KfSinusoidalGetPhase",
+ "KfSinusoidalGetWavelength",
+ "KfSinusoidalWithFrequency",
+ "KfSinusoidalWithWavelength"
+ ],
+ {
+ "title_aux": "ComfyUI-Keyframed"
+ }
+ ],
+ "https://github.com/drago87/ComfyUI_Dragos_Nodes": [
+ [
+ "file_padding",
+ "image_info",
+ "lora_loader",
+ "vae_loader"
+ ],
+ {
+ "title_aux": "ComfyUI_Dragos_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"
+ ],
+ {
+ "title_aux": "ComfyUI Easy Padding"
+ }
+ ],
+ "https://github.com/edenartlab/eden_comfy_pipelines": [
+ [
+ "CLIP_Interrogator"
+ ],
+ {
+ "title_aux": "eden_comfy_pipelines"
+ }
+ ],
+ "https://github.com/evanspearman/ComfyMath": [
+ [
+ "CM_BoolBinaryOperation",
+ "CM_BoolToInt",
+ "CM_BoolUnaryOperation",
+ "CM_BreakoutVec2",
+ "CM_BreakoutVec3",
+ "CM_BreakoutVec4",
+ "CM_ComposeVec2",
+ "CM_ComposeVec3",
+ "CM_ComposeVec4",
+ "CM_FloatBinaryCondition",
+ "CM_FloatBinaryOperation",
+ "CM_FloatToInt",
+ "CM_FloatToNumber",
+ "CM_FloatUnaryCondition",
+ "CM_FloatUnaryOperation",
+ "CM_IntBinaryCondition",
+ "CM_IntBinaryOperation",
+ "CM_IntToBool",
+ "CM_IntToFloat",
+ "CM_IntToNumber",
+ "CM_IntUnaryCondition",
+ "CM_IntUnaryOperation",
+ "CM_NearestSDXLResolution",
+ "CM_NumberBinaryCondition",
+ "CM_NumberBinaryOperation",
+ "CM_NumberToFloat",
+ "CM_NumberToInt",
+ "CM_NumberUnaryCondition",
+ "CM_NumberUnaryOperation",
+ "CM_SDXLResolution",
+ "CM_Vec2BinaryCondition",
+ "CM_Vec2BinaryOperation",
+ "CM_Vec2ScalarOperation",
+ "CM_Vec2ToScalarBinaryOperation",
+ "CM_Vec2ToScalarUnaryOperation",
+ "CM_Vec2UnaryCondition",
+ "CM_Vec2UnaryOperation",
+ "CM_Vec3BinaryCondition",
+ "CM_Vec3BinaryOperation",
+ "CM_Vec3ScalarOperation",
+ "CM_Vec3ToScalarBinaryOperation",
+ "CM_Vec3ToScalarUnaryOperation",
+ "CM_Vec3UnaryCondition",
+ "CM_Vec3UnaryOperation",
+ "CM_Vec4BinaryCondition",
+ "CM_Vec4BinaryOperation",
+ "CM_Vec4ScalarOperation",
+ "CM_Vec4ToScalarBinaryOperation",
+ "CM_Vec4ToScalarUnaryOperation",
+ "CM_Vec4UnaryCondition",
+ "CM_Vec4UnaryOperation"
+ ],
+ {
+ "title_aux": "ComfyMath"
+ }
+ ],
+ "https://github.com/fearnworks/ComfyUI_FearnworksNodes/raw/main/fw_nodes.py": [
+ [
+ "Count Files in Directory (FW)",
+ "Count Tokens (FW)",
+ "Token Count Ranker(FW)",
+ "Trim To Tokens (FW)"
+ ],
+ {
+ "title_aux": "Fearnworks Custom Nodes"
+ }
+ ],
+ "https://github.com/fexli/fexli-util-node-comfyui": [
+ [
+ "FEColor2Image",
+ "FEColorOut",
+ "FEImagePadForOutpaint",
+ "FERandomizedColor2Image"
+ ],
+ {
+ "title_aux": "fexli-util-node-comfyui"
+ }
+ ],
+ "https://github.com/filipemeneses/comfy_pixelization": [
+ [
+ "Pixelization"
+ ],
+ {
+ "title_aux": "Pixelization"
+ }
+ ],
+ "https://github.com/filliptm/ComfyUI_Fill-Nodes": [
+ [
+ "FL_ImageRandomizer"
+ ],
+ {
+ "title_aux": "ComfyUI_Fill-Nodes"
+ }
+ ],
+ "https://github.com/fitCorder/fcSuite/raw/main/fcSuite.py": [
+ [
+ "fcFloat",
+ "fcFloatMatic",
+ "fcInteger"
+ ],
+ {
+ "title_aux": "fcSuite"
+ }
+ ],
+ "https://github.com/florestefano1975/comfyui-portrait-master": [
+ [
+ "PortraitMaster"
+ ],
+ {
+ "title_aux": "comfyui-portrait-master"
+ }
+ ],
+ "https://github.com/florestefano1975/comfyui-prompt-composer": [
+ [
+ "PromptComposerAssembler",
+ "PromptComposerEffect",
+ "PromptComposerStyler",
+ "PromptComposerTextSingle",
+ "promptComposerTextMultiple"
+ ],
+ {
+ "title_aux": "comfyui-prompt-composer"
+ }
+ ],
+ "https://github.com/flyingshutter/As_ComfyUI_CustomNodes": [
+ [
+ "BatchIndex_AS",
+ "CropImage_AS",
+ "ImageMixMasked_As",
+ "ImageToMask_AS",
+ "Increment_AS",
+ "Int2Any_AS",
+ "LatentAdd_AS",
+ "LatentMixMasked_As",
+ "LatentMix_AS",
+ "LatentToImages_AS",
+ "LoadLatent_AS",
+ "MapRange_AS",
+ "MaskToImage_AS",
+ "Math_AS",
+ "NoiseImage_AS",
+ "Number2Float_AS",
+ "Number2Int_AS",
+ "Number_AS",
+ "SaveLatent_AS",
+ "TextToImage_AS",
+ "TextWildcardList_AS"
+ ],
+ {
+ "title_aux": "As_ComfyUI_CustomNodes"
+ }
+ ],
+ "https://github.com/gemell1/ComfyUI_GMIC": [
+ [
+ "GmicCliWrapper"
+ ],
+ {
+ "title_aux": "ComfyUI_GMIC"
+ }
+ ],
+ "https://github.com/giriss/comfy-image-saver": [
+ [
+ "Cfg Literal",
+ "Checkpoint Selector",
+ "Int Literal",
+ "Sampler Selector",
+ "Save Image w/Metadata",
+ "Scheduler Selector",
+ "Seed Generator",
+ "String Literal",
+ "Width/Height Literal"
+ ],
+ {
+ "title_aux": "Save Image with Generation Metadata"
+ }
+ ],
+ "https://github.com/glibsonoran/Plush-for-ComfyUI": [
+ [
+ "DalleImage",
+ "Enhancer"
+ ],
+ {
+ "title_aux": "Plush-for-ComfyUI"
+ }
+ ],
+ "https://github.com/glifxyz/ComfyUI-GlifNodes": [
+ [
+ "GlifConsistencyDecoder",
+ "GlifPatchConsistencyDecoderTiled"
+ ],
+ {
+ "title_aux": "ComfyUI-GlifNodes"
+ }
+ ],
+ "https://github.com/guoyk93/yk-node-suite-comfyui": [
+ [
+ "YKImagePadForOutpaint",
+ "YKMaskToImage"
+ ],
+ {
+ "title_aux": "y.k.'s ComfyUI node suite"
+ }
+ ],
+ "https://github.com/hhhzzyang/Comfyui_Lama": [
+ [
+ "LamaApply",
+ "LamaModelLoader",
+ "YamlConfigLoader"
+ ],
+ {
+ "title_aux": "Comfyui-Lama"
+ }
+ ],
+ "https://github.com/hnmr293/ComfyUI-nodes-hnmr": [
+ [
+ "CLIPIter",
+ "Dict2Model",
+ "GridImage",
+ "ImageBlend2",
+ "KSamplerOverrided",
+ "KSamplerSetting",
+ "KSamplerXYZ",
+ "LatentToHist",
+ "LatentToImage",
+ "ModelIter",
+ "RandomLatentImage",
+ "SaveStateDict",
+ "SaveText",
+ "StateDictLoader",
+ "StateDictMerger",
+ "StateDictMergerBlockWeighted",
+ "StateDictMergerBlockWeightedMulti",
+ "VAEDecodeBatched",
+ "VAEEncodeBatched",
+ "VAEIter"
+ ],
+ {
+ "title_aux": "ComfyUI-nodes-hnmr"
+ }
+ ],
+ "https://github.com/hustille/ComfyUI_Fooocus_KSampler": [
+ [
+ "KSampler With Refiner (Fooocus)"
+ ],
+ {
+ "title_aux": "ComfyUI_Fooocus_KSampler"
+ }
+ ],
+ "https://github.com/hustille/ComfyUI_hus_utils": [
+ [
+ "3way Prompt Styler",
+ "Batch State",
+ "Date Time Format",
+ "Debug Extra",
+ "Fetch widget value",
+ "Text Hash"
+ ],
+ {
+ "title_aux": "hus' utils for ComfyUI"
+ }
+ ],
+ "https://github.com/hylarucoder/ComfyUI-Eagle-PNGInfo": [
+ [
+ "EagleImageNode",
+ "SDXLPromptStyler",
+ "SDXLPromptStylerAdvanced",
+ "SDXLResolutionPresets"
+ ],
+ {
+ "title_aux": "Eagle PNGInfo"
+ }
+ ],
+ "https://github.com/idrirap/ComfyUI-Lora-Auto-Trigger-Words": [
+ [
+ "FusionText",
+ "LoraListNames",
+ "LoraLoaderAdvanced",
+ "LoraLoaderStackedAdvanced",
+ "LoraLoaderStackedVanilla",
+ "LoraLoaderVanilla",
+ "LoraTagsOnly",
+ "Randomizer",
+ "TagsFormater",
+ "TagsSelector",
+ "TextInputBasic"
+ ],
+ {
+ "title_aux": "ComfyUI-Lora-Auto-Trigger-Words"
+ }
+ ],
+ "https://github.com/imb101/ComfyUI-FaceSwap": [
+ [
+ "FaceSwapNode"
+ ],
+ {
+ "title_aux": "FaceSwap"
+ }
+ ],
+ "https://github.com/jags111/ComfyUI_Jags_Audiotools": [
+ [
+ "BatchJoinAudio",
+ "BatchToList",
+ "BitCrushAudioFX",
+ "BulkVariation",
+ "ChorusAudioFX",
+ "ClippingAudioFX",
+ "CompressorAudioFX",
+ "ConcatAudioList",
+ "ConvolutionAudioFX",
+ "CutAudio",
+ "DelayAudioFX",
+ "DistortionAudioFX",
+ "DuplicateAudio",
+ "GainAudioFX",
+ "GenerateAudioSample",
+ "GenerateAudioWave",
+ "GetAudioFromFolderIndex",
+ "GetSingle",
+ "GetStringByIndex",
+ "HighShelfFilter",
+ "HighpassFilter",
+ "ImageToSpectral",
+ "InvertAudioFX",
+ "JoinAudio",
+ "LadderFilter",
+ "LimiterAudioFX",
+ "ListToBatch",
+ "LoadAudioDir",
+ "LoadAudioFile",
+ "LoadAudioModel (DD)",
+ "LoadVST3",
+ "LowShelfFilter",
+ "LowpassFilter",
+ "MP3CompressorAudioFX",
+ "MixAudioTensors",
+ "NoiseGateAudioFX",
+ "OTTAudioFX",
+ "PeakFilter",
+ "PhaserEffectAudioFX",
+ "PitchShiftAudioFX",
+ "PlotSpectrogram",
+ "PreviewAudioFile",
+ "PreviewAudioTensor",
+ "ResampleAudio",
+ "ReverbAudioFX",
+ "ReverseAudio",
+ "SaveAudioTensor",
+ "SequenceVariation",
+ "SliceAudio",
+ "SoundPlayer",
+ "StretchAudio",
+ "samplerate"
+ ],
+ {
+ "author": "jags111",
+ "description": "This extension offers various audio generation tools",
+ "nickname": "Audiotools",
+ "title": "Jags_Audiotools",
+ "title_aux": "ComfyUI_Jags_Audiotools"
+ }
+ ],
+ "https://github.com/jags111/ComfyUI_Jags_VectorMagic": [
+ [
+ "CircularVAEDecode",
+ "JagsCLIPSeg",
+ "JagsClipseg",
+ "JagsCombineMasks",
+ "SVG",
+ "YoloSEGdetectionNode",
+ "YoloSegNode",
+ "color_drop",
+ "my unique name",
+ "xy_Tiling_KSampler"
+ ],
+ {
+ "author": "jags111",
+ "description": "This extension offers various vector manipulation and generation tools",
+ "nickname": "Jags_VectorMagic",
+ "title": "Jags_VectorMagic",
+ "title_aux": "ComfyUI_Jags_VectorMagic"
+ }
+ ],
+ "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.)",
+ "LatentUpscaler",
+ "LoRA Stacker",
+ "Manual XY Entry Info",
+ "NNLatentUpscale",
+ "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 Version 2.0+"
+ }
+ ],
+ "https://github.com/jamesWalker55/comfyui-various": [
+ [],
+ {
+ "nodename_pattern": "^JW",
+ "title_aux": "Various ComfyUI Nodes by Type"
+ }
+ ],
+ "https://github.com/jesenzhang/ComfyUI_StreamDiffusion": [
+ [
+ "StreamDiffusion_Loader",
+ "StreamDiffusion_Sampler"
+ ],
+ {
+ "title_aux": "ComfyUI_StreamDiffusion"
+ }
+ ],
+ "https://github.com/jitcoder/lora-info": [
+ [
+ "LoraInfo"
+ ],
+ {
+ "title_aux": "LoraInfo"
+ }
+ ],
+ "https://github.com/jjkramhoeft/ComfyUI-Jjk-Nodes": [
+ [
+ "JjkConcat",
+ "JjkShowText",
+ "JjkText",
+ "SDXLRecommendedImageSize"
+ ],
+ {
+ "title_aux": "ComfyUI-Jjk-Nodes"
+ }
+ ],
+ "https://github.com/jojkaart/ComfyUI-sampler-lcm-alternative": [
+ [
+ "LCMScheduler",
+ "SamplerLCMAlternative",
+ "SamplerLCMCycle"
+ ],
+ {
+ "title_aux": "ComfyUI-sampler-lcm-alternative"
+ }
+ ],
+ "https://github.com/jtrue/ComfyUI-JaRue": [
+ [
+ "Text2Image_jru",
+ "YouTube2Prompt_jru"
+ ],
+ {
+ "nodename_pattern": "_jru$",
+ "title_aux": "ComfyUI-JaRue"
+ }
+ ],
+ "https://github.com/ka-puna/comfyui-yanc": [
+ [
+ "YANC.ConcatStrings",
+ "YANC.FormatDatetimeString",
+ "YANC.GetWidgetValueString",
+ "YANC.IntegerCaster",
+ "YANC.MultilineString",
+ "YANC.TruncateString"
+ ],
+ {
+ "title_aux": "comfyui-yanc"
+ }
+ ],
+ "https://github.com/kenjiqq/qq-nodes-comfyui": [
+ [
+ "Any List",
+ "Axis To Float",
+ "Axis To Int",
+ "Axis To Model",
+ "Axis To Number",
+ "Axis To String",
+ "Image Accumulator End",
+ "Image Accumulator Start",
+ "Load Lines From Text File",
+ "Slice List",
+ "XY Grid Helper"
+ ],
+ {
+ "title_aux": "qq-nodes-comfyui"
+ }
+ ],
+ "https://github.com/kijai/ComfyUI-KJNodes": [
+ [
+ "AddLabel",
+ "BatchCLIPSeg",
+ "BatchCropFromMask",
+ "BatchCropFromMaskAdvanced",
+ "BatchUncrop",
+ "BatchUncropAdvanced",
+ "BboxToInt",
+ "ColorMatch",
+ "ColorToMask",
+ "ConditioningMultiCombine",
+ "ConditioningSetMaskAndCombine",
+ "ConditioningSetMaskAndCombine3",
+ "ConditioningSetMaskAndCombine4",
+ "ConditioningSetMaskAndCombine5",
+ "CreateAudioMask",
+ "CreateFadeMask",
+ "CreateFadeMaskAdvanced",
+ "CreateFluidMask",
+ "CreateGradientMask",
+ "CreateMagicMask",
+ "CreateShapeMask",
+ "CreateTextMask",
+ "CreateVoronoiMask",
+ "CrossFadeImages",
+ "DummyLatentOut",
+ "EmptyLatentImagePresets",
+ "FlipSigmasAdjusted",
+ "FloatConstant",
+ "GenerateNoise",
+ "GetImageRangeFromBatch",
+ "GetImagesFromBatchIndexed",
+ "GrowMaskWithBlur",
+ "INTConstant",
+ "ImageBatchRepeatInterleaving",
+ "ImageBatchTestPattern",
+ "ImageConcanate",
+ "ImageGrabPIL",
+ "ImageGridComposite2x2",
+ "ImageGridComposite3x3",
+ "InjectNoiseToLatent",
+ "NormalizeLatent",
+ "OffsetMask",
+ "ReferenceOnlySimple3",
+ "ReplaceImagesInBatch",
+ "ResizeMask",
+ "ReverseImageBatch",
+ "RoundMask",
+ "SaveImageWithAlpha",
+ "SomethingToString",
+ "SoundReactive",
+ "SplitBboxes",
+ "StableZero123_BatchSchedule",
+ "VRAM_Debug",
+ "WidgetToString"
+ ],
+ {
+ "title_aux": "KJNodes for ComfyUI"
+ }
+ ],
+ "https://github.com/kijai/ComfyUI-Marigold": [
+ [
+ "ColorizeDepthmap",
+ "MarigoldDepthEstimation",
+ "RemapDepth",
+ "SaveImageOpenEXR"
+ ],
+ {
+ "title_aux": "Marigold depth estimation in ComfyUI"
+ }
+ ],
+ "https://github.com/kijai/ComfyUI-SVD": [
+ [
+ "SVDimg2vid"
+ ],
+ {
+ "title_aux": "ComfyUI-SVD"
+ }
+ ],
+ "https://github.com/kinfolk0117/ComfyUI_GradientDeepShrink": [
+ [
+ "GradientPatchModelAddDownscale",
+ "GradientPatchModelAddDownscaleAdvanced"
+ ],
+ {
+ "title_aux": "ComfyUI_GradientDeepShrink"
+ }
+ ],
+ "https://github.com/kinfolk0117/ComfyUI_SimpleTiles": [
+ [
+ "TileCalc",
+ "TileMerge",
+ "TileSplit"
+ ],
+ {
+ "title_aux": "SimpleTiles"
+ }
+ ],
+ "https://github.com/kinfolk0117/ComfyUI_TiledIPAdapter": [
+ [
+ "TiledIPAdapter"
+ ],
+ {
+ "title_aux": "TiledIPAdapter"
+ }
+ ],
+ "https://github.com/knuknX/ComfyUI-Image-Tools": [
+ [
+ "BatchImagePathLoader",
+ "ImageBgRemoveProcessor",
+ "ImageStandardResizeProcessor",
+ "SingleImagePathLoader",
+ "SingleImageUrlLoader"
+ ],
+ {
+ "title_aux": "ComfyUI-Image-Tools"
+ }
+ ],
+ "https://github.com/kohya-ss/ControlNet-LLLite-ComfyUI": [
+ [
+ "LLLiteLoader"
+ ],
+ {
+ "title_aux": "ControlNet-LLLite-ComfyUI"
+ }
+ ],
+ "https://github.com/komojini/ComfyUI_SDXL_DreamBooth_LoRA_CustomNodes": [
+ [
+ "S3 Bucket LoRA",
+ "S3Bucket_Load_LoRA",
+ "XL DreamBooth LoRA",
+ "XLDB_LoRA"
+ ],
+ {
+ "title_aux": "ComfyUI_SDXL_DreamBooth_LoRA_CustomNodes"
+ }
+ ],
+ "https://github.com/kwaroran/abg-comfyui": [
+ [
+ "Remove Image Background (abg)"
+ ],
+ {
+ "title_aux": "abg-comfyui"
+ }
+ ],
+ "https://github.com/laksjdjf/LCMSampler-ComfyUI": [
+ [
+ "SamplerLCM",
+ "TAESDLoader"
+ ],
+ {
+ "title_aux": "LCMSampler-ComfyUI"
+ }
+ ],
+ "https://github.com/laksjdjf/LoRA-Merger-ComfyUI": [
+ [
+ "LoraLoaderFromWeight",
+ "LoraLoaderWeightOnly",
+ "LoraMerge",
+ "LoraSave"
+ ],
+ {
+ "title_aux": "LoRA-Merger-ComfyUI"
+ }
+ ],
+ "https://github.com/laksjdjf/attention-couple-ComfyUI": [
+ [
+ "Attention couple"
+ ],
+ {
+ "title_aux": "attention-couple-ComfyUI"
+ }
+ ],
+ "https://github.com/laksjdjf/cd-tuner_negpip-ComfyUI": [
+ [
+ "CDTuner",
+ "Negapip",
+ "Negpip"
+ ],
+ {
+ "title_aux": "cd-tuner_negpip-ComfyUI"
+ }
+ ],
+ "https://github.com/laksjdjf/pfg-ComfyUI": [
+ [
+ "PFG"
+ ],
+ {
+ "title_aux": "pfg-ComfyUI"
+ }
+ ],
+ "https://github.com/lilly1987/ComfyUI_node_Lilly": [
+ [
+ "CheckpointLoaderSimpleText",
+ "LoraLoaderText",
+ "LoraLoaderTextRandom",
+ "Random_Sampler",
+ "VAELoaderDecode"
+ ],
+ {
+ "title_aux": "simple wildcard for ComfyUI"
+ }
+ ],
+ "https://github.com/lldacing/comfyui-easyapi-nodes": [
+ [
+ "Base64ToImage",
+ "ImageToBase64",
+ "ImageToBase64Advanced",
+ "LoadImageToBase64",
+ "MaskImageToBase64",
+ "MaskToBase64",
+ "MaskToBase64Image",
+ "SamAutoMaskSEGS"
+ ],
+ {
+ "title_aux": "comfyui-easyapi-nodes"
+ }
+ ],
+ "https://github.com/lordgasmic/ComfyUI-Wildcards/raw/master/wildcards.py": [
+ [
+ "CLIPTextEncodeWithWildcards"
+ ],
+ {
+ "title_aux": "Wildcards"
+ }
+ ],
+ "https://github.com/lrzjason/ComfyUIJasonNode/raw/main/SDXLMixSampler.py": [
+ [
+ "SDXLMixSampler"
+ ],
+ {
+ "title_aux": "ComfyUIJasonNode"
+ }
+ ],
+ "https://github.com/ltdrdata/ComfyUI-Impact-Pack": [
+ [
+ "AddMask",
+ "BasicPipeToDetailerPipe",
+ "BasicPipeToDetailerPipeSDXL",
+ "BboxDetectorCombined",
+ "BboxDetectorCombined_v2",
+ "BboxDetectorForEach",
+ "BboxDetectorSEGS",
+ "BitwiseAndMask",
+ "BitwiseAndMaskForEach",
+ "CLIPSegDetectorProvider",
+ "CfgScheduleHookProvider",
+ "CombineRegionalPrompts",
+ "CoreMLDetailerHookProvider",
+ "DenoiseScheduleHookProvider",
+ "DenoiseSchedulerDetailerHookProvider",
+ "DetailerForEach",
+ "DetailerForEachDebug",
+ "DetailerForEachDebugPipe",
+ "DetailerForEachPipe",
+ "DetailerHookCombine",
+ "DetailerPipeToBasicPipe",
+ "EditBasicPipe",
+ "EditDetailerPipe",
+ "EditDetailerPipeSDXL",
+ "EmptySegs",
+ "FaceDetailer",
+ "FaceDetailerPipe",
+ "FromBasicPipe",
+ "FromBasicPipe_v2",
+ "FromDetailerPipe",
+ "FromDetailerPipeSDXL",
+ "FromDetailerPipe_v2",
+ "ImageListToImageBatch",
+ "ImageMaskSwitch",
+ "ImageReceiver",
+ "ImageSender",
+ "ImpactAssembleSEGS",
+ "ImpactCombineConditionings",
+ "ImpactCompare",
+ "ImpactConcatConditionings",
+ "ImpactConditionalBranch",
+ "ImpactConditionalStopIteration",
+ "ImpactControlBridge",
+ "ImpactControlNetApplySEGS",
+ "ImpactDecomposeSEGS",
+ "ImpactDilateMask",
+ "ImpactDilateMaskInSEGS",
+ "ImpactDilate_Mask_SEG_ELT",
+ "ImpactDummyInput",
+ "ImpactEdit_SEG_ELT",
+ "ImpactFloat",
+ "ImpactFrom_SEG_ELT",
+ "ImpactGaussianBlurMask",
+ "ImpactGaussianBlurMaskInSEGS",
+ "ImpactHFTransformersClassifierProvider",
+ "ImpactImageBatchToImageList",
+ "ImpactImageInfo",
+ "ImpactInt",
+ "ImpactInversedSwitch",
+ "ImpactIsNotEmptySEGS",
+ "ImpactKSamplerAdvancedBasicPipe",
+ "ImpactKSamplerBasicPipe",
+ "ImpactLatentInfo",
+ "ImpactLogger",
+ "ImpactMakeImageBatch",
+ "ImpactMakeImageList",
+ "ImpactMinMax",
+ "ImpactNeg",
+ "ImpactNodeSetMuteState",
+ "ImpactQueueTrigger",
+ "ImpactQueueTriggerCountdown",
+ "ImpactRemoteBoolean",
+ "ImpactRemoteInt",
+ "ImpactSEGSClassify",
+ "ImpactSEGSConcat",
+ "ImpactSEGSLabelFilter",
+ "ImpactSEGSOrderedFilter",
+ "ImpactSEGSPicker",
+ "ImpactSEGSRangeFilter",
+ "ImpactSEGSToMaskBatch",
+ "ImpactSEGSToMaskList",
+ "ImpactScaleBy_BBOX_SEG_ELT",
+ "ImpactSegsAndMask",
+ "ImpactSegsAndMaskForEach",
+ "ImpactSetWidgetValue",
+ "ImpactSimpleDetectorSEGS",
+ "ImpactSimpleDetectorSEGSPipe",
+ "ImpactSimpleDetectorSEGS_for_AD",
+ "ImpactSleep",
+ "ImpactStringSelector",
+ "ImpactSwitch",
+ "ImpactValueReceiver",
+ "ImpactValueSender",
+ "ImpactWildcardEncode",
+ "ImpactWildcardProcessor",
+ "IterativeImageUpscale",
+ "IterativeLatentUpscale",
+ "KSamplerAdvancedProvider",
+ "KSamplerProvider",
+ "LatentPixelScale",
+ "LatentReceiver",
+ "LatentSender",
+ "LatentSwitch",
+ "MMDetDetectorProvider",
+ "MMDetLoader",
+ "MaskDetailerPipe",
+ "MaskListToMaskBatch",
+ "MaskPainter",
+ "MaskToSEGS",
+ "MaskToSEGS_for_AnimateDiff",
+ "MasksToMaskList",
+ "MediaPipeFaceMeshToSEGS",
+ "NoiseInjectionDetailerHookProvider",
+ "NoiseInjectionHookProvider",
+ "ONNXDetectorProvider",
+ "ONNXDetectorSEGS",
+ "PixelKSampleHookCombine",
+ "PixelKSampleUpscalerProvider",
+ "PixelKSampleUpscalerProviderPipe",
+ "PixelTiledKSampleUpscalerProvider",
+ "PixelTiledKSampleUpscalerProviderPipe",
+ "PreviewBridge",
+ "PreviewBridgeLatent",
+ "ReencodeLatent",
+ "ReencodeLatentPipe",
+ "RegionalPrompt",
+ "RegionalSampler",
+ "RegionalSamplerAdvanced",
+ "RemoveNoiseMask",
+ "SAMDetectorCombined",
+ "SAMDetectorSegmented",
+ "SAMLoader",
+ "SEGSDetailer",
+ "SEGSDetailerForAnimateDiff",
+ "SEGSLabelFilterDetailerHookProvider",
+ "SEGSOrderedFilterDetailerHookProvider",
+ "SEGSPaste",
+ "SEGSPreview",
+ "SEGSRangeFilterDetailerHookProvider",
+ "SEGSSwitch",
+ "SEGSToImageList",
+ "SegmDetectorCombined",
+ "SegmDetectorCombined_v2",
+ "SegmDetectorForEach",
+ "SegmDetectorSEGS",
+ "Segs Mask",
+ "Segs Mask ForEach",
+ "SegsMaskCombine",
+ "SegsToCombinedMask",
+ "SetDefaultImageForSEGS",
+ "SubtractMask",
+ "SubtractMaskForEach",
+ "TiledKSamplerProvider",
+ "ToBasicPipe",
+ "ToBinaryMask",
+ "ToDetailerPipe",
+ "ToDetailerPipeSDXL",
+ "TwoAdvancedSamplersForMask",
+ "TwoSamplersForMask",
+ "TwoSamplersForMaskUpscalerProvider",
+ "TwoSamplersForMaskUpscalerProviderPipe",
+ "UltralyticsDetectorProvider",
+ "UnsamplerDetailerHookProvider",
+ "UnsamplerHookProvider"
+ ],
+ {
+ "author": "Dr.Lt.Data",
+ "description": "This extension offers various detector nodes and detailer nodes that allow you to configure a workflow that automatically enhances facial details. And provide iterative upscaler.",
+ "nickname": "Impact Pack",
+ "title": "Impact Pack",
+ "title_aux": "ComfyUI Impact Pack"
+ }
+ ],
+ "https://github.com/ltdrdata/ComfyUI-Inspire-Pack": [
+ [
+ "AnimeLineArt_Preprocessor_Provider_for_SEGS //Inspire",
+ "ApplyRegionalIPAdapters //Inspire",
+ "BindImageListPromptList //Inspire",
+ "CLIPTextEncodeWithWeight //Inspire",
+ "CacheBackendData //Inspire",
+ "CacheBackendDataList //Inspire",
+ "CacheBackendDataNumberKey //Inspire",
+ "CacheBackendDataNumberKeyList //Inspire",
+ "Canny_Preprocessor_Provider_for_SEGS //Inspire",
+ "ChangeImageBatchSize //Inspire",
+ "CheckpointLoaderSimpleShared //Inspire",
+ "Color_Preprocessor_Provider_for_SEGS //Inspire",
+ "ConcatConditioningsWithMultiplier //Inspire",
+ "DWPreprocessor_Provider_for_SEGS //Inspire",
+ "FakeScribblePreprocessor_Provider_for_SEGS //Inspire",
+ "FloatRange //Inspire",
+ "FromIPAdapterPipe //Inspire",
+ "GlobalSampler //Inspire",
+ "GlobalSeed //Inspire",
+ "HEDPreprocessor_Provider_for_SEGS //Inspire",
+ "InpaintPreprocessor_Provider_for_SEGS //Inspire",
+ "KSampler //Inspire",
+ "KSamplerAdvanced //Inspire",
+ "KSamplerAdvancedProgress //Inspire",
+ "KSamplerProgress //Inspire",
+ "LeRes_DepthMap_Preprocessor_Provider_for_SEGS //Inspire",
+ "LineArt_Preprocessor_Provider_for_SEGS //Inspire",
+ "ListCounter //Inspire",
+ "LoadImage //Inspire",
+ "LoadImageListFromDir //Inspire",
+ "LoadImagesFromDir //Inspire",
+ "LoadPromptsFromDir //Inspire",
+ "LoadPromptsFromFile //Inspire",
+ "LoadSinglePromptFromFile //Inspire",
+ "LoraBlockInfo //Inspire",
+ "LoraLoaderBlockWeight //Inspire",
+ "Manga2Anime_LineArt_Preprocessor_Provider_for_SEGS //Inspire",
+ "MediaPipeFaceMeshDetectorProvider //Inspire",
+ "MediaPipe_FaceMesh_Preprocessor_Provider_for_SEGS //Inspire",
+ "MiDaS_DepthMap_Preprocessor_Provider_for_SEGS //Inspire",
+ "OpenPose_Preprocessor_Provider_for_SEGS //Inspire",
+ "PromptBuilder //Inspire",
+ "PromptExtractor //Inspire",
+ "RegionalConditioningColorMask //Inspire",
+ "RegionalConditioningSimple //Inspire",
+ "RegionalIPAdapterColorMask //Inspire",
+ "RegionalIPAdapterEncodedColorMask //Inspire",
+ "RegionalIPAdapterEncodedMask //Inspire",
+ "RegionalIPAdapterMask //Inspire",
+ "RegionalPromptColorMask //Inspire",
+ "RegionalPromptSimple //Inspire",
+ "RegionalSeedExplorerColorMask //Inspire",
+ "RegionalSeedExplorerMask //Inspire",
+ "RemoveBackendData //Inspire",
+ "RemoveBackendDataNumberKey //Inspire",
+ "RetrieveBackendData //Inspire",
+ "RetrieveBackendDataNumberKey //Inspire",
+ "SeedExplorer //Inspire",
+ "ShowCachedInfo //Inspire",
+ "TilePreprocessor_Provider_for_SEGS //Inspire",
+ "ToIPAdapterPipe //Inspire",
+ "UnzipPrompt //Inspire",
+ "WildcardEncode //Inspire",
+ "XY Input: Lora Block Weight //Inspire",
+ "ZipPrompt //Inspire",
+ "Zoe_DepthMap_Preprocessor_Provider_for_SEGS //Inspire"
+ ],
+ {
+ "author": "Dr.Lt.Data",
+ "description": "This extension provides various nodes to support Lora Block Weight and the Impact Pack.",
+ "nickname": "Inspire Pack",
+ "nodename_pattern": "Inspire$",
+ "title": "Inspire Pack",
+ "title_aux": "ComfyUI Inspire Pack"
+ }
+ ],
+ "https://github.com/m-sokes/ComfyUI-Sokes-Nodes": [
+ [
+ "Custom Date Format | sokes \ud83e\uddac",
+ "Latent Switch x9 | sokes \ud83e\uddac"
+ ],
+ {
+ "title_aux": "ComfyUI Sokes Nodes"
+ }
+ ],
+ "https://github.com/m957ymj75urz/ComfyUI-Custom-Nodes/raw/main/clip-text-encode-split/clip_text_encode_split.py": [
+ [
+ "RawText",
+ "RawTextCombine",
+ "RawTextEncode",
+ "RawTextReplace"
+ ],
+ {
+ "title_aux": "m957ymj75urz/ComfyUI-Custom-Nodes"
+ }
+ ],
+ "https://github.com/marhensa/sdxl-recommended-res-calc": [
+ [
+ "RecommendedResCalc"
+ ],
+ {
+ "title_aux": "Recommended Resolution Calculator"
+ }
+ ],
+ "https://github.com/martijnat/comfyui-previewlatent": [
+ [
+ "PreviewLatent",
+ "PreviewLatentAdvanced"
+ ],
+ {
+ "title_aux": "comfyui-previewlatent"
+ }
+ ],
+ "https://github.com/matan1905/ComfyUI-Serving-Toolkit": [
+ [
+ "DiscordServing",
+ "ServingInputNumber",
+ "ServingInputText",
+ "ServingOutput",
+ "WebSocketServing"
+ ],
+ {
+ "title_aux": "ComfyUI Serving toolkit"
+ }
+ ],
+ "https://github.com/mav-rik/facerestore_cf": [
+ [
+ "CropFace",
+ "FaceRestoreCFWithModel",
+ "FaceRestoreModelLoader"
+ ],
+ {
+ "title_aux": "Facerestore CF (Code Former)"
+ }
+ ],
+ "https://github.com/mcmonkeyprojects/sd-dynamic-thresholding": [
+ [
+ "DynamicThresholdingFull",
+ "DynamicThresholdingSimple"
+ ],
+ {
+ "title_aux": "Stable Diffusion Dynamic Thresholding (CFG Scale Fix)"
+ }
+ ],
+ "https://github.com/meap158/ComfyUI-Background-Replacement": [
+ [
+ "BackgroundReplacement",
+ "ImageComposite"
+ ],
+ {
+ "title_aux": "ComfyUI-Background-Replacement"
+ }
+ ],
+ "https://github.com/meap158/ComfyUI-GPU-temperature-protection": [
+ [
+ "GPUTemperatureProtection"
+ ],
+ {
+ "title_aux": "GPU temperature protection"
+ }
+ ],
+ "https://github.com/meap158/ComfyUI-Prompt-Expansion": [
+ [
+ "PromptExpansion"
+ ],
+ {
+ "title_aux": "ComfyUI-Prompt-Expansion"
+ }
+ ],
+ "https://github.com/melMass/comfy_mtb": [
+ [
+ "Animation Builder (mtb)",
+ "Any To String (mtb)",
+ "Batch Float (mtb)",
+ "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)",
+ "Bbox From Mask (mtb)",
+ "Blur (mtb)",
+ "Color Correct (mtb)",
+ "Colored Image (mtb)",
+ "Concat Images (mtb)",
+ "Crop (mtb)",
+ "Debug (mtb)",
+ "Deep Bump (mtb)",
+ "Export With Ffmpeg (mtb)",
+ "Face Swap (mtb)",
+ "Film Interpolation (mtb)",
+ "Fit Number (mtb)",
+ "Float To Number (mtb)",
+ "Get Batch From History (mtb)",
+ "Image Compare (mtb)",
+ "Image Premultiply (mtb)",
+ "Image Remove Background Rembg (mtb)",
+ "Image Resize Factor (mtb)",
+ "Image Tile Offset (mtb)",
+ "Int To Bool (mtb)",
+ "Int To Number (mtb)",
+ "Interpolate Clip Sequential (mtb)",
+ "Latent Lerp (mtb)",
+ "Load Face Analysis Model (mtb)",
+ "Load Face Enhance Model (mtb)",
+ "Load Face Swap Model (mtb)",
+ "Load Film Model (mtb)",
+ "Load Image From Url (mtb)",
+ "Load Image Sequence (mtb)",
+ "Mask To Image (mtb)",
+ "Math Expression (mtb)",
+ "Model Patch Seamless (mtb)",
+ "Pick From Batch (mtb)",
+ "Qr Code (mtb)",
+ "Restore Face (mtb)",
+ "Save Gif (mtb)",
+ "Save Image Grid (mtb)",
+ "Save Image Sequence (mtb)",
+ "Save Tensors (mtb)",
+ "Sharpen (mtb)",
+ "Smart Step (mtb)",
+ "Stack Images (mtb)",
+ "String Replace (mtb)",
+ "Styles Loader (mtb)",
+ "Text To Image (mtb)",
+ "Transform Image (mtb)",
+ "Uncrop (mtb)",
+ "Unsplash Image (mtb)",
+ "Vae Decode (mtb)"
+ ],
+ {
+ "nodename_pattern": "\\(mtb\\)$",
+ "title_aux": "MTB Nodes"
+ }
+ ],
+ "https://github.com/mihaiiancu/ComfyUI_Inpaint": [
+ [
+ "InpaintMediapipe"
+ ],
+ {
+ "title_aux": "mihaiiancu/Inpaint"
+ }
+ ],
+ "https://github.com/mikkel/ComfyUI-text-overlay": [
+ [
+ "Image Text Overlay"
+ ],
+ {
+ "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",
+ "lamaPreprocessor"
+ ],
+ {
+ "title_aux": "LaMa Preprocessor [WIP]"
+ }
+ ],
+ "https://github.com/modusCell/ComfyUI-dimension-node-modusCell": [
+ [
+ "DimensionProviderFree modusCell",
+ "DimensionProviderRatio modusCell",
+ "String Concat modusCell"
+ ],
+ {
+ "title_aux": "Preset Dimensions"
+ }
+ ],
+ "https://github.com/mpiquero7164/ComfyUI-SaveImgPrompt": [
+ [
+ "Save IMG Prompt"
+ ],
+ {
+ "title_aux": "SaveImgPrompt"
+ }
+ ],
+ "https://github.com/nagolinc/ComfyUI_FastVAEDecorder_SDXL": [
+ [
+ "FastLatentToImage"
+ ],
+ {
+ "title_aux": "ComfyUI_FastVAEDecorder_SDXL"
+ }
+ ],
+ "https://github.com/natto-maki/ComfyUI-NegiTools": [
+ [
+ "NegiTools_CompositeImages",
+ "NegiTools_DepthEstimationByMarigold",
+ "NegiTools_ImageProperties",
+ "NegiTools_LatentProperties",
+ "NegiTools_NoiseImageGenerator",
+ "NegiTools_OpenAiDalle3",
+ "NegiTools_OpenAiTranslate",
+ "NegiTools_OpenPoseToPointList",
+ "NegiTools_PointListToMask",
+ "NegiTools_RandomImageLoader",
+ "NegiTools_SaveImageToDirectory",
+ "NegiTools_SeedGenerator",
+ "NegiTools_StereoImageGenerator",
+ "NegiTools_StringFunction"
+ ],
+ {
+ "title_aux": "ComfyUI-NegiTools"
+ }
+ ],
+ "https://github.com/nicolai256/comfyUI_Nodes_nicolai256/raw/main/yugioh-presets.py": [
+ [
+ "yugioh_Presets"
+ ],
+ {
+ "title_aux": "comfyUI_Nodes_nicolai256"
+ }
+ ],
+ "https://github.com/ningxiaoxiao/comfyui-NDI": [
+ [
+ "NDI_LoadImage",
+ "NDI_SendImage"
+ ],
+ {
+ "title_aux": "comfyui-NDI"
+ }
+ ],
+ "https://github.com/noembryo/ComfyUI-noEmbryo": [
+ [
+ "PromptTermList1",
+ "PromptTermList2",
+ "PromptTermList3",
+ "PromptTermList4",
+ "PromptTermList5",
+ "PromptTermList6"
+ ],
+ {
+ "author": "noEmbryo",
+ "description": "Some useful nodes for ComfyUI",
+ "nickname": "noEmbryo",
+ "title": "noEmbryo nodes for ComfyUI",
+ "title_aux": "noEmbryo nodes"
+ }
+ ],
+ "https://github.com/noxinias/ComfyUI_NoxinNodes": [
+ [
+ "NoxinChime",
+ "NoxinPromptLoad",
+ "NoxinPromptSave",
+ "NoxinScaledResolution",
+ "NoxinSimpleMath",
+ "NoxinSplitPrompt"
+ ],
+ {
+ "title_aux": "ComfyUI_NoxinNodes"
+ }
+ ],
+ "https://github.com/ntdviet/comfyui-ext/raw/main/custom_nodes/gcLatentTunnel/gcLatentTunnel.py": [
+ [
+ "gcLatentTunnel"
+ ],
+ {
+ "title_aux": "ntdviet/comfyui-ext"
+ }
+ ],
+ "https://github.com/omar92/ComfyUI-QualityOfLifeSuit_Omar92": [
+ [
+ "CLIPStringEncode _O",
+ "Chat completion _O",
+ "ChatGPT Simple _O",
+ "ChatGPT _O",
+ "ChatGPT compact _O",
+ "Chat_Completion _O",
+ "Chat_Message _O",
+ "Chat_Message_fromString _O",
+ "Concat Text _O",
+ "ConcatRandomNSP_O",
+ "Debug String _O",
+ "Debug Text _O",
+ "Debug Text route _O",
+ "Edit_image _O",
+ "Equation1param _O",
+ "Equation2params _O",
+ "GetImage_(Width&Height) _O",
+ "GetLatent_(Width&Height) _O",
+ "ImageScaleFactor _O",
+ "ImageScaleFactorSimple _O",
+ "LatentUpscaleFactor _O",
+ "LatentUpscaleFactorSimple _O",
+ "LatentUpscaleMultiply",
+ "Note _O",
+ "RandomNSP _O",
+ "Replace Text _O",
+ "String _O",
+ "Text _O",
+ "Text2Image _O",
+ "Trim Text _O",
+ "VAEDecodeParallel _O",
+ "combine_chat_messages _O",
+ "compine_chat_messages _O",
+ "concat Strings _O",
+ "create image _O",
+ "create_image _O",
+ "debug Completeion _O",
+ "debug messages_O",
+ "float _O",
+ "floatToInt _O",
+ "floatToText _O",
+ "int _O",
+ "intToFloat _O",
+ "load_openAI _O",
+ "replace String _O",
+ "replace String advanced _O",
+ "saveTextToFile _O",
+ "seed _O",
+ "selectLatentFromBatch _O",
+ "string2Image _O",
+ "trim String _O",
+ "variation_image _O"
+ ],
+ {
+ "title_aux": "Quality of life Suit:V2"
+ }
+ ],
+ "https://github.com/ostris/ostris_nodes_comfyui": [
+ [
+ "LLM Pipe Loader - Ostris",
+ "LLM Prompt Upsampling - Ostris",
+ "One Seed - Ostris",
+ "Text Box - Ostris"
+ ],
+ {
+ "nodename_pattern": "- Ostris$",
+ "title_aux": "Ostris Nodes ComfyUI"
+ }
+ ],
+ "https://github.com/oyvindg/ComfyUI-TrollSuite": [
+ [
+ "BinaryImageMask",
+ "ImagePadding",
+ "LoadLastImage",
+ "RandomMask",
+ "TransparentImage"
+ ],
+ {
+ "title_aux": "ComfyUI-TrollSuite"
+ }
+ ],
+ "https://github.com/palant/extended-saveimage-comfyui": [
+ [
+ "SaveImageExtended"
+ ],
+ {
+ "title_aux": "Extended Save Image for ComfyUI"
+ }
+ ],
+ "https://github.com/palant/image-resize-comfyui": [
+ [
+ "ImageResize"
+ ],
+ {
+ "title_aux": "Image Resize for ComfyUI"
+ }
+ ],
+ "https://github.com/pants007/comfy-pants": [
+ [
+ "CLIPTextEncodeAIO",
+ "Image Make Square"
+ ],
+ {
+ "title_aux": "pants"
+ }
+ ],
+ "https://github.com/paulo-coronado/comfy_clip_blip_node": [
+ [
+ "CLIPTextEncodeBLIP",
+ "CLIPTextEncodeBLIP-2",
+ "Example"
+ ],
+ {
+ "title_aux": "comfy_clip_blip_node"
+ }
+ ],
+ "https://github.com/picturesonpictures/comfy_PoP": [
+ [
+ "AdaptiveCannyDetector_PoP",
+ "AnyAspectRatio",
+ "ConditioningMultiplier_PoP",
+ "ConditioningNormalizer_PoP",
+ "LoadImageResizer_PoP",
+ "LoraStackLoader10_PoP",
+ "LoraStackLoader_PoP",
+ "VAEDecoderPoP",
+ "VAEEncoderPoP"
+ ],
+ {
+ "title_aux": "comfy_PoP"
+ }
+ ],
+ "https://github.com/pkpkTech/ComfyUI-SaveAVIF": [
+ [
+ "SaveAvif"
+ ],
+ {
+ "title_aux": "ComfyUI-SaveAVIF"
+ }
+ ],
+ "https://github.com/pythongosssss/ComfyUI-Custom-Scripts": [
+ [
+ "CheckpointLoader|pysssss",
+ "ConstrainImage|pysssss",
+ "LoadText|pysssss",
+ "LoraLoader|pysssss",
+ "MathExpression|pysssss",
+ "MultiPrimitive|pysssss",
+ "PlaySound|pysssss",
+ "Repeater|pysssss",
+ "ReroutePrimitive|pysssss",
+ "SaveText|pysssss",
+ "ShowText|pysssss",
+ "StringFunction|pysssss"
+ ],
+ {
+ "title_aux": "pythongosssss/ComfyUI-Custom-Scripts"
+ }
+ ],
+ "https://github.com/pythongosssss/ComfyUI-WD14-Tagger": [
+ [
+ "WD14Tagger|pysssss"
+ ],
+ {
+ "title_aux": "ComfyUI WD 1.4 Tagger"
+ }
+ ],
+ "https://github.com/ramyma/A8R8_ComfyUI_nodes": [
+ [
+ "Base64ImageInput",
+ "Base64ImageOutput"
+ ],
+ {
+ "title_aux": "A8R8 ComfyUI Nodes"
+ }
+ ],
+ "https://github.com/rcfcu2000/zhihuige-nodes-comfyui": [
+ [
+ "Combine ZHGMasks",
+ "Cover ZHGMasks",
+ "ZHG FaceIndex",
+ "ZHG GetMaskArea",
+ "ZHG SaveImage",
+ "ZHG SmoothEdge"
+ ],
+ {
+ "title_aux": "zhihuige-nodes-comfyui"
+ }
+ ],
+ "https://github.com/rcsaquino/comfyui-custom-nodes": [
+ [
+ "BackgroundRemover | rcsaquino",
+ "VAELoader | rcsaquino",
+ "VAEProcessor | rcsaquino"
+ ],
+ {
+ "title_aux": "rcsaquino/comfyui-custom-nodes"
+ }
+ ],
+ "https://github.com/receyuki/comfyui-prompt-reader-node": [
+ [
+ "SDBatchLoader",
+ "SDParameterExtractor",
+ "SDParameterGenerator",
+ "SDPromptMerger",
+ "SDPromptReader",
+ "SDPromptSaver",
+ "SDTypeConverter"
+ ],
+ {
+ "author": "receyuki",
+ "description": "ComfyUI node version of the SD Prompt Reader",
+ "nickname": "SD Prompt Reader",
+ "title": "SD Prompt Reader",
+ "title_aux": "comfyui-prompt-reader-node"
+ }
+ ],
+ "https://github.com/rgthree/rgthree-comfy": [
+ [],
+ {
+ "author": "rgthree",
+ "description": "A bunch of nodes I created that I also find useful.",
+ "nickname": "rgthree",
+ "nodename_pattern": " \\(rgthree\\)$",
+ "title": "Comfy Nodes",
+ "title_aux": "rgthree's ComfyUI Nodes"
+ }
+ ],
+ "https://github.com/richinsley/Comfy-LFO": [
+ [
+ "LFO_Pulse",
+ "LFO_Sawtooth",
+ "LFO_Sine",
+ "LFO_Square",
+ "LFO_Triangle"
+ ],
+ {
+ "title_aux": "Comfy-LFO"
+ }
+ ],
+ "https://github.com/rklaffehn/rk-comfy-nodes": [
+ [
+ "RK_CivitAIAddHashes",
+ "RK_CivitAIMetaChecker"
+ ],
+ {
+ "title_aux": "rk-comfy-nodes"
+ }
+ ],
+ "https://github.com/romeobuilderotti/ComfyUI-PNG-Metadata": [
+ [
+ "SetMetadataAll",
+ "SetMetadataString"
+ ],
+ {
+ "title_aux": "ComfyUI PNG Metadata"
+ }
+ ],
+ "https://github.com/rui40000/RUI-Nodes": [
+ [
+ "ABCondition",
+ "CharacterCount"
+ ],
+ {
+ "title_aux": "RUI-Nodes"
+ }
+ ],
+ "https://github.com/s1dlx/comfy_meh/raw/main/meh.py": [
+ [
+ "MergingExecutionHelper"
+ ],
+ {
+ "title_aux": "comfy_meh"
+ }
+ ],
+ "https://github.com/seanlynch/comfyui-optical-flow": [
+ [
+ "Apply optical flow",
+ "Compute optical flow",
+ "Visualize optical flow"
+ ],
+ {
+ "title_aux": "ComfyUI Optical Flow"
+ }
+ ],
+ "https://github.com/seanlynch/srl-nodes": [
+ [
+ "SRL Conditional Interrrupt",
+ "SRL Eval",
+ "SRL Filter Image List",
+ "SRL Format String"
+ ],
+ {
+ "title_aux": "SRL's nodes"
+ }
+ ],
+ "https://github.com/sergekatzmann/ComfyUI_Nimbus-Pack": [
+ [
+ "ImageResizeAndCropNode",
+ "ImageSquareAdapterNode"
+ ],
+ {
+ "title_aux": "ComfyUI_Nimbus-Pack"
+ }
+ ],
+ "https://github.com/shadowcz007/comfyui-mixlab-nodes": [
+ [
+ "3DImage",
+ "AppInfo",
+ "AreaToMask",
+ "CLIPSeg",
+ "CLIPSeg_",
+ "CharacterInText",
+ "ChatGPTOpenAI",
+ "Color",
+ "CombineMasks_",
+ "CombineSegMasks",
+ "DynamicDelayProcessor",
+ "EnhanceImage",
+ "FaceToMask",
+ "FeatheredMask",
+ "FloatSlider",
+ "FloatingVideo",
+ "Font",
+ "GamePal",
+ "GetImageSize_",
+ "ImageCropByAlpha",
+ "IntNumber",
+ "LimitNumber",
+ "LoadImagesFromPath",
+ "LoadImagesFromURL",
+ "MergeLayers",
+ "MultiplicationNode",
+ "NewLayer",
+ "NoiseImage",
+ "RandomPrompt",
+ "ResizeImageMixlab",
+ "ScreenShare",
+ "ShowLayer",
+ "ShowTextForGPT",
+ "SmoothMask",
+ "SpeechRecognition",
+ "SpeechSynthesis",
+ "SplitLongMask",
+ "SvgImage",
+ "SwitchByIndex",
+ "TextImage",
+ "TextInput_",
+ "TextToNumber",
+ "TransparentImage",
+ "VAEDecodeConsistencyDecoder",
+ "VAELoaderConsistencyDecoder"
+ ],
+ {
+ "title_aux": "comfyui-mixlab-nodes"
+ }
+ ],
+ "https://github.com/shiimizu/ComfyUI_smZNodes": [
+ [
+ "smZ CLIPTextEncode",
+ "smZ Settings"
+ ],
+ {
+ "title_aux": "smZNodes"
+ }
+ ],
+ "https://github.com/shingo1228/ComfyUI-SDXL-EmptyLatentImage": [
+ [
+ "SDXL Empty Latent Image"
+ ],
+ {
+ "title_aux": "ComfyUI-SDXL-EmptyLatentImage"
+ }
+ ],
+ "https://github.com/shingo1228/ComfyUI-send-eagle-slim": [
+ [
+ "Send Webp Image to Eagle"
+ ],
+ {
+ "title_aux": "ComfyUI-send-Eagle(slim)"
+ }
+ ],
+ "https://github.com/shockz0rz/ComfyUI_InterpolateEverything": [
+ [
+ "OpenposePreprocessorInterpolate"
+ ],
+ {
+ "title_aux": "InterpolateEverything"
+ }
+ ],
+ "https://github.com/shockz0rz/comfy-easy-grids": [
+ [
+ "FloatToText",
+ "GridFloatList",
+ "GridFloats",
+ "GridIntList",
+ "GridInts",
+ "GridStringList",
+ "GridStrings",
+ "ImageGridCommander",
+ "IntToText",
+ "SaveImageGrid",
+ "TextConcatenator"
+ ],
+ {
+ "title_aux": "comfy-easy-grids"
+ }
+ ],
+ "https://github.com/sipherxyz/comfyui-art-venture": [
+ [
+ "AV_CheckpointMerge",
+ "AV_CheckpointModelsToParametersPipe",
+ "AV_CheckpointSave",
+ "AV_ControlNetEfficientLoader",
+ "AV_ControlNetEfficientLoaderAdvanced",
+ "AV_ControlNetEfficientStacker",
+ "AV_ControlNetEfficientStackerSimple",
+ "AV_ControlNetLoader",
+ "AV_ControlNetPreprocessor",
+ "AV_LoraListLoader",
+ "AV_LoraListStacker",
+ "AV_LoraLoader",
+ "AV_ParametersPipeToCheckpointModels",
+ "AV_ParametersPipeToPrompts",
+ "AV_PromptsToParametersPipe",
+ "AV_SAMLoader",
+ "AV_VAELoader",
+ "AspectRatioSelector",
+ "BLIPCaption",
+ "BLIPLoader",
+ "BooleanPrimitive",
+ "ColorBlend",
+ "ColorCorrect",
+ "DeepDanbooruCaption",
+ "DependenciesEdit",
+ "Fooocus_KSampler",
+ "Fooocus_KSamplerAdvanced",
+ "GetBoolFromJson",
+ "GetFloatFromJson",
+ "GetIntFromJson",
+ "GetObjectFromJson",
+ "GetSAMEmbedding",
+ "GetTextFromJson",
+ "ISNetLoader",
+ "ISNetSegment",
+ "ImageAlphaComposite",
+ "ImageApplyChannel",
+ "ImageExtractChannel",
+ "ImageGaussianBlur",
+ "ImageMuxer",
+ "ImageRepeat",
+ "ImageScaleDown",
+ "ImageScaleDownBy",
+ "ImageScaleDownToSize",
+ "ImageScaleToMegapixels",
+ "LaMaInpaint",
+ "LoadImageAsMaskFromUrl",
+ "LoadImageFromUrl",
+ "LoadJsonFromUrl",
+ "MergeModels",
+ "NumberScaler",
+ "OverlayInpaintedImage",
+ "OverlayInpaintedLatent",
+ "PrepareImageAndMaskForInpaint",
+ "QRCodeGenerator",
+ "RandomFloat",
+ "RandomInt",
+ "SAMEmbeddingToImage",
+ "SDXLAspectRatioSelector",
+ "SDXLPromptStyler",
+ "SeedSelector",
+ "StringToInt",
+ "StringToNumber"
+ ],
+ {
+ "title_aux": "comfyui-art-venture"
+ }
+ ],
+ "https://github.com/skfoo/ComfyUI-Coziness": [
+ [
+ "LoraTextExtractor-b1f83aa2",
+ "MultiLoraLoader-70bf3d77"
+ ],
+ {
+ "title_aux": "ComfyUI-Coziness"
+ }
+ ],
+ "https://github.com/space-nuko/ComfyUI-Disco-Diffusion": [
+ [
+ "DiscoDiffusion_DiscoDiffusion",
+ "DiscoDiffusion_DiscoDiffusionExtraSettings",
+ "DiscoDiffusion_GuidedDiffusionLoader",
+ "DiscoDiffusion_OpenAICLIPLoader"
+ ],
+ {
+ "title_aux": "Disco Diffusion"
+ }
+ ],
+ "https://github.com/space-nuko/ComfyUI-OpenPose-Editor": [
+ [
+ "Nui.OpenPoseEditor"
+ ],
+ {
+ "title_aux": "OpenPose Editor"
+ }
+ ],
+ "https://github.com/space-nuko/nui-suite": [
+ [
+ "Nui.DynamicPromptsTextGen",
+ "Nui.FeelingLuckyTextGen",
+ "Nui.OutputString"
+ ],
+ {
+ "title_aux": "nui suite"
+ }
+ ],
+ "https://github.com/spacepxl/ComfyUI-HQ-Image-Save": [
+ [
+ "LoadLatentEXR",
+ "SaveEXR",
+ "SaveLatentEXR",
+ "SaveTiff"
+ ],
+ {
+ "title_aux": "ComfyUI-HQ-Image-Save"
+ }
+ ],
+ "https://github.com/spacepxl/ComfyUI-Image-Filters": [
+ [
+ "AlphaClean",
+ "AlphaMatte",
+ "BlurImageFast",
+ "BlurMaskFast",
+ "DilateErodeMask",
+ "EnhanceDetail",
+ "GuidedFilterAlpha",
+ "RemapRange"
+ ],
+ {
+ "title_aux": "ComfyUI-Image-Filters"
+ }
+ ],
+ "https://github.com/spinagon/ComfyUI-seam-carving": [
+ [
+ "SeamCarving"
+ ],
+ {
+ "title_aux": "ComfyUI-seam-carving"
+ }
+ ],
+ "https://github.com/spinagon/ComfyUI-seamless-tiling": [
+ [
+ "CircularVAEDecode",
+ "MakeCircularVAE",
+ "OffsetImage",
+ "SeamlessTile"
+ ],
+ {
+ "title_aux": "Seamless tiling Node for ComfyUI"
+ }
+ ],
+ "https://github.com/spro/comfyui-mirror": [
+ [
+ "LatentMirror"
+ ],
+ {
+ "title_aux": "Latent Mirror node for ComfyUI"
+ }
+ ],
+ "https://github.com/ssitu/ComfyUI_UltimateSDUpscale": [
+ [
+ "UltimateSDUpscale",
+ "UltimateSDUpscaleNoUpscale"
+ ],
+ {
+ "title_aux": "UltimateSDUpscale"
+ }
+ ],
+ "https://github.com/ssitu/ComfyUI_fabric": [
+ [
+ "FABRICPatchModel",
+ "FABRICPatchModelAdv",
+ "KSamplerAdvFABRICAdv",
+ "KSamplerFABRIC",
+ "KSamplerFABRICAdv"
+ ],
+ {
+ "title_aux": "ComfyUI fabric"
+ }
+ ],
+ "https://github.com/ssitu/ComfyUI_restart_sampling": [
+ [
+ "KRestartSampler",
+ "KRestartSamplerAdv",
+ "KRestartSamplerSimple"
+ ],
+ {
+ "title_aux": "Restart Sampling"
+ }
+ ],
+ "https://github.com/ssitu/ComfyUI_roop": [
+ [
+ "RoopImproved",
+ "roop"
+ ],
+ {
+ "title_aux": "ComfyUI roop"
+ }
+ ],
+ "https://github.com/storyicon/comfyui_segment_anything": [
+ [
+ "GroundingDinoModelLoader (segment anything)",
+ "GroundingDinoSAMSegment (segment anything)",
+ "InvertMask (segment anything)",
+ "SAMModelLoader (segment anything)"
+ ],
+ {
+ "title_aux": "segment anything"
+ }
+ ],
+ "https://github.com/strimmlarn/ComfyUI_Strimmlarns_aesthetic_score": [
+ [
+ "AesthetlcScoreSorter",
+ "CalculateAestheticScore",
+ "LoadAesteticModel",
+ "ScoreToNumber"
+ ],
+ {
+ "title_aux": "ComfyUI_Strimmlarns_aesthetic_score"
+ }
+ ],
+ "https://github.com/styler00dollar/ComfyUI-deepcache": [
+ [
+ "DeepCache"
+ ],
+ {
+ "title_aux": "ComfyUI-deepcache"
+ }
+ ],
+ "https://github.com/styler00dollar/ComfyUI-sudo-latent-upscale": [
+ [
+ "SudoLatentUpscale"
+ ],
+ {
+ "title_aux": "ComfyUI-sudo-latent-upscale"
+ }
+ ],
+ "https://github.com/syllebra/bilbox-comfyui": [
+ [
+ "BilboXLut",
+ "BilboXPhotoPrompt",
+ "BilboXVignette"
+ ],
+ {
+ "title_aux": "BilboX's ComfyUI Custom Nodes"
+ }
+ ],
+ "https://github.com/sylym/comfy_vid2vid": [
+ [
+ "CheckpointLoaderSimpleSequence",
+ "DdimInversionSequence",
+ "KSamplerSequence",
+ "LoadImageMaskSequence",
+ "LoadImageSequence",
+ "LoraLoaderSequence",
+ "SetLatentNoiseSequence",
+ "TrainUnetSequence",
+ "VAEEncodeForInpaintSequence"
+ ],
+ {
+ "title_aux": "Vid2vid"
+ }
+ ],
+ "https://github.com/szhublox/ambw_comfyui": [
+ [
+ "Auto Merge Block Weighted",
+ "CLIPMergeSimple",
+ "CheckpointSave",
+ "ModelMergeBlocks",
+ "ModelMergeSimple"
+ ],
+ {
+ "title_aux": "Auto-MBW"
+ }
+ ],
+ "https://github.com/taabata/Comfy_Syrian_Falcon_Nodes/raw/main/SyrianFalconNodes.py": [
+ [
+ "CompositeImage",
+ "KSamplerAlternate",
+ "KSamplerPromptEdit",
+ "KSamplerPromptEditAndAlternate",
+ "LoopBack",
+ "QRGenerate",
+ "WordAsImage"
+ ],
+ {
+ "title_aux": "Syrian Falcon Nodes"
+ }
+ ],
+ "https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy": [
+ [
+ "FreeU_LCM",
+ "ImageOutputToComfyNodes",
+ "ImageShuffle",
+ "LCMGenerate",
+ "LCMGenerate_ReferenceOnly",
+ "LCMGenerate_SDTurbo",
+ "LCMGenerate_img2img",
+ "LCMGenerate_img2img_IPAdapter",
+ "LCMGenerate_img2img_controlnet",
+ "LCMGenerate_inpaintv2",
+ "LCMGenerate_inpaintv3",
+ "LCMLoader",
+ "LCMLoader_RefInpaint",
+ "LCMLoader_ReferenceOnly",
+ "LCMLoader_SDTurbo",
+ "LCMLoader_controlnet",
+ "LCMLoader_controlnet_inpaint",
+ "LCMLoader_img2img",
+ "LCMLoraLoader_inpaint",
+ "LCMLora_inpaint",
+ "LCMT2IAdapter",
+ "LCM_IPAdapter",
+ "LCM_IPAdapter_inpaint",
+ "LCM_outpaint_prep",
+ "LoadImageNode_LCM",
+ "Loader_SegmindVega",
+ "OutpaintCanvasTool",
+ "SaveImage_LCM",
+ "SaveImage_Puzzle",
+ "SaveImage_PuzzleV2",
+ "SegmindVega",
+ "stitch"
+ ],
+ {
+ "title_aux": "LCM_Inpaint-Outpaint_Comfy"
+ }
+ ],
+ "https://github.com/theUpsider/ComfyUI-Logic": [
+ [
+ "Bool",
+ "Compare",
+ "DebugPrint",
+ "Float",
+ "If ANY execute A else B",
+ "Int",
+ "String"
+ ],
+ {
+ "title_aux": "ComfyUI-Logic"
+ }
+ ],
+ "https://github.com/theUpsider/ComfyUI-Styles_CSV_Loader": [
+ [
+ "Load Styles CSV"
+ ],
+ {
+ "title_aux": "Styles CSV Loader Extension for ComfyUI"
+ }
+ ],
+ "https://github.com/thecooltechguy/ComfyUI-MagicAnimate": [
+ [
+ "MagicAnimate",
+ "MagicAnimateModelLoader"
+ ],
+ {
+ "title_aux": "ComfyUI-MagicAnimate"
+ }
+ ],
+ "https://github.com/thecooltechguy/ComfyUI-Stable-Video-Diffusion": [
+ [
+ "SVDDecoder",
+ "SVDModelLoader",
+ "SVDSampler",
+ "SVDSimpleImg2Vid"
+ ],
+ {
+ "title_aux": "ComfyUI Stable Video Diffusion"
+ }
+ ],
+ "https://github.com/thedyze/save-image-extended-comfyui": [
+ [
+ "SaveImageExtended"
+ ],
+ {
+ "title_aux": "Save Image Extended for ComfyUI"
+ }
+ ],
+ "https://github.com/toyxyz/ComfyUI_toyxyz_test_nodes": [
+ [
+ "CaptureWebcam",
+ "LatentDelay",
+ "LoadWebcamImage",
+ "SaveImagetoPath"
+ ],
+ {
+ "title_aux": "ComfyUI_toyxyz_test_nodes"
+ }
+ ],
+ "https://github.com/trojblue/trNodes": [
+ [
+ "JpgConvertNode",
+ "trColorCorrection",
+ "trLayering",
+ "trRouter",
+ "trRouterLonger"
+ ],
+ {
+ "title_aux": "trNodes"
+ }
+ ],
+ "https://github.com/ttulttul/ComfyUI-Iterative-Mixer": [
+ [
+ "Batch Unsampler",
+ "Iterative Mixing KSampler",
+ "Iterative Mixing KSampler Advanced",
+ "Latent Batch Comparison Plot",
+ "Latent Batch Statistics Plot"
+ ],
+ {
+ "title_aux": "ComfyUI Iterative Mixing Nodes"
+ }
+ ],
+ "https://github.com/tudal/Hakkun-ComfyUI-nodes/raw/main/hakkun_nodes.py": [
+ [
+ "Any Converter",
+ "Calculate Upscale",
+ "Image Resize To Height",
+ "Image Resize To Width",
+ "Image size to string",
+ "Load Random Image",
+ "Load Text",
+ "Multi Text Merge",
+ "Prompt Parser",
+ "Random Line",
+ "Random Line 4"
+ ],
+ {
+ "title_aux": "Hakkun-ComfyUI-nodes"
+ }
+ ],
+ "https://github.com/tusharbhutt/Endless-Nodes": [
+ [
+ "ESS Aesthetic Scoring",
+ "ESS Aesthetic Scoring Auto",
+ "ESS Combo Parameterizer",
+ "ESS Combo Parameterizer & Prompts",
+ "ESS Eight Input Random",
+ "ESS Eight Input Text Switch",
+ "ESS Float to Integer",
+ "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",
+ "ESS Integer to Float",
+ "ESS Integer to Number",
+ "ESS Integer to String",
+ "ESS Integer to X",
+ "ESS Number to Float",
+ "ESS Number to Integer",
+ "ESS Number to String",
+ "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",
+ "ESS Six Integer IO Widget",
+ "ESS String to Float",
+ "ESS String to Integer",
+ "ESS String to Num",
+ "ESS String to X",
+ "\u267e\ufe0f\ud83c\udf0a\u2728 Image Saver with JSON"
+ ],
+ {
+ "author": "BiffMunky",
+ "description": "A small set of nodes I created for various numerical and text inputs. Features image saver with ability to have JSON saved to separate folder, parameter collection nodes, two aesthetic scoring models, switches for text and numbers, and conversion of string to numeric and vice versa.",
+ "nickname": "\u267e\ufe0f\ud83c\udf0a\u2728",
+ "title": "Endless \ufe0f\ud83c\udf0a\u2728 Nodes",
+ "title_aux": "Endless \ufe0f\ud83c\udf0a\u2728 Nodes"
+ }
+ ],
+ "https://github.com/twri/sdxl_prompt_styler": [
+ [
+ "SDXLPromptStyler",
+ "SDXLPromptStylerAdvanced"
+ ],
+ {
+ "title_aux": "SDXL Prompt Styler"
+ }
+ ],
+ "https://github.com/uarefans/ComfyUI-Fans": [
+ [
+ "Fans Prompt Styler Negative",
+ "Fans Prompt Styler Positive",
+ "Fans Styler",
+ "Fans Text Concatenate"
+ ],
+ {
+ "title_aux": "ComfyUI-Fans"
+ }
+ ],
+ "https://github.com/vanillacode314/SimpleWildcardsComfyUI": [
+ [
+ "SimpleConcat",
+ "SimpleWildcard"
+ ],
+ {
+ "author": "VanillaCode314",
+ "description": "A simple wildcard node for ComfyUI. Can also be used a style prompt node.",
+ "nickname": "Simple Wildcard",
+ "title": "Simple Wildcard",
+ "title_aux": "Simple Wildcard"
+ }
+ ],
+ "https://github.com/vienteck/ComfyUI-Chat-GPT-Integration": [
+ [
+ "ChatGptPrompt",
+ "ChatGptTextConcat"
+ ],
+ {
+ "title_aux": "ComfyUI-Chat-GPT-Integration"
+ }
+ ],
+ "https://github.com/violet-chen/comfyui-psd2png": [
+ [
+ "Psd2Png"
+ ],
+ {
+ "title_aux": "comfyui-psd2png"
+ }
+ ],
+ "https://github.com/wallish77/wlsh_nodes": [
+ [
+ "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)",
+ "CLIP Positive-Negative w/Text (WLSH)",
+ "Checkpoint Loader w/Name (WLSH)",
+ "Empty Latent by Pixels (WLSH)",
+ "Empty Latent by Ratio (WLSH)",
+ "Empty Latent by Size (WLSH)",
+ "Generate Border Mask (WLSH)",
+ "Grayscale Image (WLSH)",
+ "Image Load with Metadata (WLSH)",
+ "Image Save with Prompt (WLSH)",
+ "Image Save with Prompt File (WLSH)",
+ "Image Save with Prompt/Info (WLSH)",
+ "Image Save with Prompt/Info File (WLSH)",
+ "Image Scale By Factor (WLSH)",
+ "Image Scale by Shortside (WLSH)",
+ "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)",
+ "SDXL Quick Image Scale (WLSH)",
+ "SDXL Resolutions (WLSH)",
+ "SDXL Steps (WLSH)",
+ "Save Positive Prompt(WLSH)",
+ "Save Prompt (WLSH)",
+ "Save Prompt/Info (WLSH)",
+ "Seed and Int (WLSH)",
+ "Seed to Number (WLSH)",
+ "Simple Pattern Replace (WLSH)",
+ "Simple String Combine (WLSH)",
+ "Time String (WLSH)",
+ "Upscale by Factor with Model (WLSH)",
+ "VAE Encode for Inpaint w/Padding (WLSH)"
+ ],
+ {
+ "title_aux": "wlsh_nodes"
+ }
+ ],
+ "https://github.com/whatbirdisthat/cyberdolphin": [
+ [
+ "\ud83d\udc2c Gradio ChatInterface",
+ "\ud83d\udc2c OpenAI Advanced",
+ "\ud83d\udc2c OpenAI Compatible",
+ "\ud83d\udc2c OpenAI DALL\u00b7E",
+ "\ud83d\udc2c OpenAI Simple"
+ ],
+ {
+ "title_aux": "cyberdolphin"
+ }
+ ],
+ "https://github.com/whmc76/ComfyUI-Openpose-Editor-Plus": [
+ [
+ "CDL.OpenPoseEditorPlus"
+ ],
+ {
+ "title_aux": "ComfyUI-Openpose-Editor-Plus"
+ }
+ ],
+ "https://github.com/wmatson/easy-comfy-nodes": [
+ [
+ "EZAssocDictNode",
+ "EZAssocImgNode",
+ "EZAssocStrNode",
+ "EZEmptyDictNode",
+ "EZHttpPostNode",
+ "EZLoadImgBatchFromUrlsNode",
+ "EZLoadImgFromUrlNode",
+ "EZRemoveImgBackground",
+ "EZVideoCombiner"
+ ],
+ {
+ "title_aux": "easy-comfy-nodes"
+ }
+ ],
+ "https://github.com/wolfden/ComfyUi_PromptStylers": [
+ [
+ "SDXLPromptStylerAll",
+ "SDXLPromptStylerHorror",
+ "SDXLPromptStylerMisc",
+ "SDXLPromptStylerbyArtist",
+ "SDXLPromptStylerbyCamera",
+ "SDXLPromptStylerbyComposition",
+ "SDXLPromptStylerbyCyberpunkSurrealism",
+ "SDXLPromptStylerbyDepth",
+ "SDXLPromptStylerbyEnvironment",
+ "SDXLPromptStylerbyFantasySetting",
+ "SDXLPromptStylerbyFilter",
+ "SDXLPromptStylerbyFocus",
+ "SDXLPromptStylerbyImpressionism",
+ "SDXLPromptStylerbyLighting",
+ "SDXLPromptStylerbyMileHigh",
+ "SDXLPromptStylerbyMood",
+ "SDXLPromptStylerbyMythicalCreature",
+ "SDXLPromptStylerbyOriginal",
+ "SDXLPromptStylerbyQuantumRealism",
+ "SDXLPromptStylerbySteamPunkRealism",
+ "SDXLPromptStylerbySubject",
+ "SDXLPromptStylerbySurrealism",
+ "SDXLPromptStylerbyTheme",
+ "SDXLPromptStylerbyTimeofDay",
+ "SDXLPromptStylerbyWyvern",
+ "SDXLPromptbyCelticArt",
+ "SDXLPromptbyContemporaryNordicArt",
+ "SDXLPromptbyFashionArt",
+ "SDXLPromptbyGothicRevival",
+ "SDXLPromptbyIrishFolkArt",
+ "SDXLPromptbyRomanticNationalismArt",
+ "SDXLPromptbySportsArt",
+ "SDXLPromptbyStreetArt",
+ "SDXLPromptbyVikingArt",
+ "SDXLPromptbyWildlifeArt"
+ ],
+ {
+ "title_aux": "SDXL Prompt Styler (customized version by wolfden)"
+ }
+ ],
+ "https://github.com/wolfden/ComfyUi_String_Function_Tree": [
+ [
+ "StringFunction"
+ ],
+ {
+ "title_aux": "ComfyUi_String_Function_Tree"
+ }
+ ],
+ "https://github.com/wsippel/comfyui_ws/raw/main/sdxl_utility.py": [
+ [
+ "SDXLResolutionPresets"
+ ],
+ {
+ "title_aux": "SDXLResolutionPresets"
+ }
+ ],
+ "https://github.com/wutipong/ComfyUI-TextUtils": [
+ [
+ "Text Utils - Join N-Elements of String List",
+ "Text Utils - Join String List",
+ "Text Utils - Join Strings",
+ "Text Utils - Split String to List"
+ ],
+ {
+ "title_aux": "ComfyUI-TextUtils"
+ }
+ ],
+ "https://github.com/xXAdonesXx/NodeGPT": [
+ [
+ "AppendAgent",
+ "Assistant",
+ "Chat",
+ "ChatGPT",
+ "CombineInput",
+ "Conditioning",
+ "CostumeAgent_1",
+ "CostumeAgent_2",
+ "CostumeMaster_1",
+ "Critic",
+ "DisplayString",
+ "DisplayTextAsImage",
+ "EVAL",
+ "Engineer",
+ "Executor",
+ "GroupChat",
+ "Image_generation_Conditioning",
+ "LM_Studio",
+ "LoadAPIconfig",
+ "LoadTXT",
+ "MemGPT",
+ "Memory_Excel",
+ "Model_1",
+ "Ollama",
+ "Output2String",
+ "Planner",
+ "Scientist",
+ "TextCombine",
+ "TextGeneration",
+ "TextGenerator",
+ "TextInput",
+ "TextOutput",
+ "UserProxy",
+ "llama-cpp",
+ "llava",
+ "oobaboogaOpenAI"
+ ],
+ {
+ "title_aux": "NodeGPT"
+ }
+ ],
+ "https://github.com/yolain/ComfyUI-Easy-Use": [
+ [
+ "dynamicThresholdingFull",
+ "easy LLLiteLoader",
+ "easy XYPlot",
+ "easy a1111Loader",
+ "easy comfyLoader",
+ "easy controlnetLoader",
+ "easy controlnetLoaderADV",
+ "easy detailerFix",
+ "easy fullLoader",
+ "easy fullkSampler",
+ "easy globalSeed",
+ "easy hiresFix",
+ "easy imageInsetCrop",
+ "easy imagePixelPerfect",
+ "easy imageRemoveBG",
+ "easy imageSize",
+ "easy imageSizeByLongerSide",
+ "easy imageSizeBySide",
+ "easy kSampler",
+ "easy kSamplerSDTurbo",
+ "easy kSamplerTiled",
+ "easy loraStack",
+ "easy negative",
+ "easy pipeIn",
+ "easy pipeOut",
+ "easy portraitMaster",
+ "easy poseEditor",
+ "easy positive",
+ "easy preDetailerFix",
+ "easy preSampling",
+ "easy preSamplingAdvanced",
+ "easy preSamplingDynamicCFG",
+ "easy preSamplingSdTurbo",
+ "easy samLoaderPipe",
+ "easy seed",
+ "easy showSpentTime",
+ "easy svdLoader",
+ "easy ultralyticsDetectorPipe",
+ "easy wildcards",
+ "easy zero123Loader"
+ ],
+ {
+ "title_aux": "ComfyUI Easy Use"
+ }
+ ],
+ "https://github.com/yolanother/DTAIComfyImageSubmit": [
+ [
+ "DTSimpleSubmitImage",
+ "DTSubmitImage"
+ ],
+ {
+ "title_aux": "Comfy AI DoubTech.ai Image Sumission Node"
+ }
+ ],
+ "https://github.com/yolanother/DTAIComfyLoaders": [
+ [
+ "DTCLIPLoader",
+ "DTCLIPVisionLoader",
+ "DTCheckpointLoader",
+ "DTCheckpointLoaderSimple",
+ "DTControlNetLoader",
+ "DTDiffControlNetLoader",
+ "DTDiffusersLoader",
+ "DTGLIGENLoader",
+ "DTLoadImage",
+ "DTLoadImageMask",
+ "DTLoadLatent",
+ "DTLoraLoader",
+ "DTLorasLoader",
+ "DTStyleModelLoader",
+ "DTUpscaleModelLoader",
+ "DTVAELoader",
+ "DTunCLIPCheckpointLoader"
+ ],
+ {
+ "title_aux": "Comfy UI Online Loaders"
+ }
+ ],
+ "https://github.com/yolanother/DTAIComfyPromptAgent": [
+ [
+ "DTPromptAgent",
+ "DTPromptAgentString"
+ ],
+ {
+ "title_aux": "Comfy UI Prompt Agent"
+ }
+ ],
+ "https://github.com/yolanother/DTAIComfyQRCodes": [
+ [
+ "QRCode"
+ ],
+ {
+ "title_aux": "Comfy UI QR Codes"
+ }
+ ],
+ "https://github.com/yolanother/DTAIComfyVariables": [
+ [
+ "DTCLIPTextEncode",
+ "DTSingleLineStringVariable",
+ "DTSingleLineStringVariableNoClip",
+ "FloatVariable",
+ "IntVariable",
+ "StringFormat",
+ "StringFormatSingleLine",
+ "StringVariable"
+ ],
+ {
+ "title_aux": "Variables for Comfy UI"
+ }
+ ],
+ "https://github.com/yolanother/DTAIImageToTextNode": [
+ [
+ "DTAIImageToTextNode",
+ "DTAIImageUrlToTextNode"
+ ],
+ {
+ "title_aux": "Image to Text Node"
+ }
+ ],
+ "https://github.com/youyegit/tdxh_node_comfyui": [
+ [
+ "TdxhBoolNumber",
+ "TdxhClipVison",
+ "TdxhControlNetApply",
+ "TdxhControlNetProcessor",
+ "TdxhFloatInput",
+ "TdxhImageToSize",
+ "TdxhImageToSizeAdvanced",
+ "TdxhImg2ImgLatent",
+ "TdxhIntInput",
+ "TdxhLoraLoader",
+ "TdxhOnOrOff",
+ "TdxhReference",
+ "TdxhStringInput",
+ "TdxhStringInputTranslator"
+ ],
+ {
+ "title_aux": "tdxh_node_comfyui"
+ }
+ ],
+ "https://github.com/zcfrank1st/Comfyui-Toolbox": [
+ [
+ "PreviewJson",
+ "PreviewVideo",
+ "SaveJson",
+ "TestJsonPreview"
+ ],
+ {
+ "title_aux": "Comfyui-Toolbox"
+ }
+ ],
+ "https://github.com/zcfrank1st/Comfyui-Yolov8": [
+ [
+ "Yolov8Detection",
+ "Yolov8Segmentation"
+ ],
+ {
+ "title_aux": "ComfyUI Yolov8"
+ }
+ ],
+ "https://github.com/zcfrank1st/comfyui_visual_anagrams": [
+ [
+ "VisualAnagramsAnimate",
+ "VisualAnagramsSample"
+ ],
+ {
+ "title_aux": "comfyui_visual_anagram"
+ }
+ ],
+ "https://github.com/zer0TF/cute-comfy": [
+ [
+ "Cute.Placeholder"
+ ],
+ {
+ "title_aux": "Cute Comfy"
+ }
+ ],
+ "https://github.com/zfkun/ComfyUI_zfkun": [
+ [
+ "ZFLoadImagePath",
+ "ZFPreviewText",
+ "ZFPreviewTextMultiline",
+ "ZFShareScreen",
+ "ZFTextTranslation"
+ ],
+ {
+ "title_aux": "ComfyUI_zfkun"
+ }
+ ],
+ "https://github.com/zhuanqianfish/ComfyUI-EasyNode": [
+ [
+ "EasyCaptureNode",
+ "EasyVideoOutputNode",
+ "SendImageWebSocket"
+ ],
+ {
+ "title_aux": "EasyCaptureNode for ComfyUI"
+ }
+ ],
+ "https://raw.githubusercontent.com/throttlekitty/SDXLCustomAspectRatio/main/SDXLAspectRatio.py": [
+ [
+ "SDXLAspectRatio"
+ ],
+ {
+ "title_aux": "SDXLCustomAspectRatio"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/model-list.json b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/model-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..aa6e85514fdc8f050675e672fae2c4e01de699d3
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/new/model-list.json
@@ -0,0 +1,754 @@
+{
+ "models": [
+ {
+ "name": "ip-adapter-faceid_sd15.bin",
+ "type": "IP-Adapter",
+ "base": "SD1.5",
+ "save_path": "ipadapter",
+ "description": "IP-Adapter-FaceID Model (SD1.5)",
+ "reference": "https://huggingface.co/h94/IP-Adapter-FaceID",
+ "filename": "ip-adapter-faceid_sd15.bin",
+ "url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid_sd15.bin"
+ },
+ {
+ "name": "ip-adapter-faceid_sd15_lora.safetensors",
+ "type": "lora",
+ "base": "SD1.5",
+ "save_path": "loras/ipadapter",
+ "description": "IP-Adapter-FaceID LoRA Model (SD1.5)",
+ "reference": "https://huggingface.co/h94/IP-Adapter-FaceID",
+ "filename": "ip-adapter-faceid_sd15_lora.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid_sd15_lora.safetensors"
+ },
+
+
+ {
+ "name": "LongAnimatediff/lt_long_mm_16_64_frames_v1.1.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "animatediff",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/models",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/Lightricks/LongAnimateDiff",
+ "filename": "lt_long_mm_16_64_frames_v1.1.ckpt",
+ "url": "https://huggingface.co/Lightricks/LongAnimateDiff/resolve/main/lt_long_mm_16_64_frames_v1.1.ckpt"
+ },
+
+ {
+ "name": "animatediff/v3_sd15_sparsectrl_rgb.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "controlnet",
+ "base": "SD1.x",
+ "save_path": "controlnet/SD1.5/animatediff",
+ "description": "AnimateDiff SparseCtrl RGB ControlNet model",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v3_sd15_sparsectrl_rgb.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v3_sd15_sparsectrl_rgb.ckpt"
+ },
+ {
+ "name": "animatediff/v3_sd15_sparsectrl_scribble.ckpt",
+ "type": "controlnet",
+ "base": "SD1.x",
+ "save_path": "controlnet/SD1.5/animatediff",
+ "description": "AnimateDiff SparseCtrl Scribble ControlNet model",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v3_sd15_sparsectrl_scribble.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v3_sd15_sparsectrl_scribble.ckpt"
+ },
+ {
+ "name": "animatediff/v3_sd15_mm.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "animatediff",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/models",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v3_sd15_mm.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v3_sd15_mm.ckpt"
+ },
+ {
+ "name": "animatediff/v3_sd15_adapter.ckpt",
+ "type": "lora",
+ "base": "SD1.x",
+ "save_path": "loras/SD1.5/animatediff",
+ "description": "AnimateDiff Adapter LoRA (SD1.5)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v3_sd15_adapter.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v3_sd15_adapter.ckpt"
+ },
+
+ {
+ "name": "Segmind-Vega",
+ "type": "checkpoint",
+ "base": "segmind-vega",
+ "save_path": "checkpoints/segmind-vega",
+ "description": "The Segmind-Vega Model is a distilled version of the Stable Diffusion XL (SDXL), offering a remarkable 70% reduction in size and an impressive 100% speedup while retaining high-quality text-to-image generation capabilities.",
+ "reference": "https://huggingface.co/segmind/Segmind-Vega",
+ "filename": "segmind-vega.safetensors",
+ "url": "https://huggingface.co/segmind/Segmind-Vega/resolve/main/segmind-vega.safetensors"
+ },
+ {
+ "name": "Segmind-VegaRT - Latent Consistency Model (LCM) LoRA of Segmind-Vega",
+ "type": "lora",
+ "base": "segmind-vega",
+ "save_path": "loras/segmind-vega",
+ "description": "Segmind-VegaRT a distilled consistency adapter for Segmind-Vega that allows to reduce the number of inference steps to only between 2 - 8 steps.",
+ "reference": "https://huggingface.co/segmind/Segmind-VegaRT",
+ "filename": "pytorch_lora_weights.safetensors",
+ "url": "https://huggingface.co/segmind/Segmind-VegaRT/resolve/main/pytorch_lora_weights.safetensors"
+ },
+
+ {
+ "name": "stabilityai/Stable Zero123",
+ "type": "zero123",
+ "base": "zero123",
+ "save_path": "checkpoints/zero123",
+ "description": "Stable Zero123 is a model for view-conditioned image generation based on [a/Zero123](https://github.com/cvlab-columbia/zero123).",
+ "reference": "https://huggingface.co/stabilityai/stable-zero123",
+ "filename": "stable_zero123.ckpt",
+ "url": "https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt"
+ },
+ {
+ "name": "LongAnimatediff/lt_long_mm_32_frames.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "animatediff",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/models",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/Lightricks/LongAnimateDiff",
+ "filename": "lt_long_mm_32_frames.ckpt",
+ "url": "https://huggingface.co/Lightricks/LongAnimateDiff/resolve/main/lt_long_mm_32_frames.ckpt"
+ },
+ {
+ "name": "LongAnimatediff/lt_long_mm_16_64_frames.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "animatediff",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/models",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/Lightricks/LongAnimateDiff",
+ "filename": "lt_long_mm_16_64_frames.ckpt",
+ "url": "https://huggingface.co/Lightricks/LongAnimateDiff/resolve/main/lt_long_mm_16_64_frames.ckpt"
+ },
+ {
+ "name": "ip-adapter_sd15.safetensors",
+ "type": "IP-Adapter",
+ "base": "SD1.5",
+ "save_path": "ipadapter",
+ "description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "ip-adapter_sd15.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15.safetensors"
+ },
+ {
+ "name": "ip-adapter_sd15_light.safetensors",
+ "type": "IP-Adapter",
+ "base": "SD1.5",
+ "save_path": "ipadapter",
+ "description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "ip-adapter_sd15_light.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15_light.safetensors"
+ },
+ {
+ "name": "ip-adapter_sd15_vit-G.safetensors",
+ "type": "IP-Adapter",
+ "base": "SD1.5",
+ "save_path": "ipadapter",
+ "description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "ip-adapter_sd15_vit-G.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15_vit-G.safetensors"
+ },
+ {
+ "name": "ip-adapter-plus_sd15.safetensors",
+ "type": "IP-Adapter",
+ "base": "SD1.5",
+ "save_path": "ipadapter",
+ "description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "ip-adapter-plus_sd15.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter-plus_sd15.safetensors"
+ },
+ {
+ "name": "ip-adapter-plus-face_sd15.safetensors",
+ "type": "IP-Adapter",
+ "base": "SD1.5",
+ "save_path": "ipadapter",
+ "description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "ip-adapter-plus-face_sd15.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter-plus-face_sd15.safetensors"
+ },
+ {
+ "name": "ip-adapter-full-face_sd15.safetensors",
+ "type": "IP-Adapter",
+ "base": "SD1.5",
+ "save_path": "ipadapter",
+ "description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "ip-adapter-full-face_sd15.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter-full-face_sd15.safetensors"
+ },
+ {
+ "name": "ip-adapter_sdxl.safetensors",
+ "type": "IP-Adapter",
+ "base": "SDXL",
+ "save_path": "ipadapter",
+ "description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "ip-adapter_sdxl.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter_sdxl.safetensors"
+ },
+ {
+ "name": "ip-adapter_sdxl_vit-h.safetensors",
+ "type": "IP-Adapter",
+ "base": "SDXL",
+ "save_path": "ipadapter",
+ "description": "This model requires the use of the SD1.5 encoder despite being for SDXL checkpoints",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "ip-adapter_sdxl_vit-h.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter_sdxl_vit-h.safetensors"
+ },
+ {
+ "name": "ip-adapter-plus_sdxl_vit-h.safetensors",
+ "type": "IP-Adapter",
+ "base": "SDXL",
+ "save_path": "ipadapter",
+ "description": "This model requires the use of the SD1.5 encoder despite being for SDXL checkpoints",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "ip-adapter-plus_sdxl_vit-h.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter-plus_sdxl_vit-h.safetensors"
+ },
+ {
+ "name": "ip-adapter-plus-face_sdxl_vit-h.safetensors",
+ "type": "IP-Adapter",
+ "base": "SDXL",
+ "save_path": "ipadapter",
+ "description": "This model requires the use of the SD1.5 encoder despite being for SDXL checkpoints",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "ip-adapter-plus-face_sdxl_vit-h.safetensors",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter-plus-face_sdxl_vit-h.safetensors"
+ },
+
+ {
+ "name": "SDXL-Turbo 1.0 (fp16)",
+ "type": "checkpoints",
+ "base": "SDXL",
+ "save_path": "checkpoints/SDXL-TURBO",
+ "description": "[6.9GB] SDXL-Turbo 1.0 fp16",
+ "reference": "https://huggingface.co/stabilityai/sdxl-turbo",
+ "filename": "sd_xl_turbo_1.0_fp16.safetensors",
+ "url": "https://huggingface.co/stabilityai/sdxl-turbo/resolve/main/sd_xl_turbo_1.0_fp16.safetensors"
+ },
+ {
+ "name": "SDXL-Turbo 1.0",
+ "type": "checkpoints",
+ "base": "SDXL",
+ "save_path": "checkpoints/SDXL-TURBO",
+ "description": "[13.9GB] SDXL-Turbo 1.0",
+ "reference": "https://huggingface.co/stabilityai/sdxl-turbo",
+ "filename": "sd_xl_turbo_1.0.safetensors",
+ "url": "https://huggingface.co/stabilityai/sdxl-turbo/resolve/main/sd_xl_turbo_1.0.safetensors"
+ },
+ {
+ "name": "Stable Video Diffusion Image-to-Video",
+ "type": "checkpoints",
+ "base": "SVD",
+ "save_path": "checkpoints/SVD",
+ "description": "Stable Video Diffusion (SVD) Image-to-Video is a diffusion model that takes in a still image as a conditioning frame, and generates a video from it. NOTE: 14 frames @ 576x1024",
+ "reference": "https://huggingface.co/stabilityai/stable-video-diffusion-img2vid",
+ "filename": "svd.safetensors",
+ "url": "https://huggingface.co/stabilityai/stable-video-diffusion-img2vid/resolve/main/svd.safetensors"
+ },
+ {
+ "name": "Stable Video Diffusion Image-to-Video (XT)",
+ "type": "checkpoints",
+ "base": "SVD",
+ "save_path": "checkpoints/SVD",
+ "description": "Stable Video Diffusion (SVD) Image-to-Video is a diffusion model that takes in a still image as a conditioning frame, and generates a video from it. NOTE: 25 frames @ 576x1024 ",
+ "reference": "https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt",
+ "filename": "svd_xt.safetensors",
+ "url": "https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt/resolve/main/svd_xt.safetensors"
+ },
+
+ {
+ "name": "animatediff/mm_sdxl_v10_beta.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "animatediff",
+ "base": "SDXL",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/models",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "mm_sdxl_v10_beta.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/mm_sdxl_v10_beta.ckpt"
+ },
+ {
+ "name": "animatediff/v2_lora_PanLeft.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "motion lora",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/motion_lora",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v2_lora_PanLeft.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v2_lora_PanLeft.ckpt"
+ },
+ {
+ "name": "animatediff/v2_lora_PanRight.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "motion lora",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/motion_lora",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v2_lora_PanRight.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v2_lora_PanRight.ckpt"
+ },
+ {
+ "name": "animatediff/v2_lora_RollingAnticlockwise.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "motion lora",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/motion_lora",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v2_lora_RollingAnticlockwise.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v2_lora_RollingAnticlockwise.ckpt"
+ },
+ {
+ "name": "animatediff/v2_lora_RollingClockwise.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "motion lora",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/motion_lora",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v2_lora_RollingClockwise.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v2_lora_RollingClockwise.ckpt"
+ },
+ {
+ "name": "animatediff/v2_lora_TiltDown.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "motion lora",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/motion_lora",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v2_lora_TiltDown.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v2_lora_TiltDown.ckpt"
+ },
+ {
+ "name": "animatediff/v2_lora_TiltUp.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "motion lora",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/motion_lora",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v2_lora_TiltUp.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v2_lora_TiltUp.ckpt"
+ },
+ {
+ "name": "animatediff/v2_lora_ZoomIn.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "motion lora",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/motion_lora",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v2_lora_ZoomIn.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v2_lora_ZoomIn.ckpt"
+ },
+ {
+ "name": "animatediff/v2_lora_ZoomOut.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "motion lora",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/motion_lora",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "v2_lora_ZoomOut.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/v2_lora_ZoomOut.ckpt"
+ },
+
+ {
+ "name": "CiaraRowles/TemporalNet1XL (1.0)",
+ "type": "controlnet",
+ "base": "SD1.5",
+ "save_path": "controlnet/TemporalNet1XL",
+ "description": "This is TemporalNet1XL, it is a re-train of the controlnet TemporalNet1 with Stable Diffusion XL.",
+ "reference": "https://huggingface.co/CiaraRowles/controlnet-temporalnet-sdxl-1.0",
+ "filename": "diffusion_pytorch_model.safetensors",
+ "url": "https://huggingface.co/CiaraRowles/controlnet-temporalnet-sdxl-1.0/resolve/main/diffusion_pytorch_model.safetensors"
+ },
+
+ {
+ "name": "LCM LoRA SD1.5",
+ "type": "lora",
+ "base": "SD1.5",
+ "save_path": "loras/lcm/SD1.5",
+ "description": "Latent Consistency LoRA for SD1.5",
+ "reference": "https://huggingface.co/latent-consistency/lcm-lora-sdv1-5",
+ "filename": "pytorch_lora_weights.safetensors",
+ "url": "https://huggingface.co/latent-consistency/lcm-lora-sdv1-5/resolve/main/pytorch_lora_weights.safetensors"
+ },
+ {
+ "name": "LCM LoRA SSD-1B",
+ "type": "lora",
+ "base": "SSD-1B",
+ "save_path": "loras/lcm/SSD-1B",
+ "description": "Latent Consistency LoRA for SSD-1B",
+ "reference": "https://huggingface.co/latent-consistency/lcm-lora-ssd-1b",
+ "filename": "pytorch_lora_weights.safetensors",
+ "url": "https://huggingface.co/latent-consistency/lcm-lora-ssd-1b/resolve/main/pytorch_lora_weights.safetensors"
+ },
+ {
+ "name": "LCM LoRA SDXL",
+ "type": "lora",
+ "base": "SSD-1B",
+ "save_path": "loras/lcm/SDXL",
+ "description": "Latent Consistency LoRA for SDXL",
+ "reference": "https://huggingface.co/latent-consistency/lcm-lora-sdxl",
+ "filename": "pytorch_lora_weights.safetensors",
+ "url": "https://huggingface.co/latent-consistency/lcm-lora-sdxl/resolve/main/pytorch_lora_weights.safetensors"
+ },
+
+ {
+ "name": "face_yolov8m-seg_60.pt (segm)",
+ "type": "Ultralytics",
+ "base": "Ultralytics",
+ "save_path": "ultralytics/segm",
+ "description": "These are the available models in the UltralyticsDetectorProvider of Impact Pack.",
+ "reference": "https://github.com/hben35096/assets/releases/tag/yolo8",
+ "filename": "face_yolov8m-seg_60.pt",
+ "url": "https://github.com/hben35096/assets/releases/download/yolo8/face_yolov8m-seg_60.pt"
+ },
+ {
+ "name": "face_yolov8n-seg2_60.pt (segm)",
+ "type": "Ultralytics",
+ "base": "Ultralytics",
+ "save_path": "ultralytics/segm",
+ "description": "These are the available models in the UltralyticsDetectorProvider of Impact Pack.",
+ "reference": "https://github.com/hben35096/assets/releases/tag/yolo8",
+ "filename": "face_yolov8n-seg2_60.pt",
+ "url": "https://github.com/hben35096/assets/releases/download/yolo8/face_yolov8n-seg2_60.pt"
+ },
+ {
+ "name": "hair_yolov8n-seg_60.pt (segm)",
+ "type": "Ultralytics",
+ "base": "Ultralytics",
+ "save_path": "ultralytics/segm",
+ "description": "These are the available models in the UltralyticsDetectorProvider of Impact Pack.",
+ "reference": "https://github.com/hben35096/assets/releases/tag/yolo8",
+ "filename": "hair_yolov8n-seg_60.pt",
+ "url": "https://github.com/hben35096/assets/releases/download/yolo8/hair_yolov8n-seg_60.pt"
+ },
+ {
+ "name": "skin_yolov8m-seg_400.pt (segm)",
+ "type": "Ultralytics",
+ "base": "Ultralytics",
+ "save_path": "ultralytics/segm",
+ "description": "These are the available models in the UltralyticsDetectorProvider of Impact Pack.",
+ "reference": "https://github.com/hben35096/assets/releases/tag/yolo8",
+ "filename": "skin_yolov8m-seg_400.pt",
+ "url": "https://github.com/hben35096/assets/releases/download/yolo8/skin_yolov8m-seg_400.pt"
+ },
+ {
+ "name": "skin_yolov8n-seg_400.pt (segm)",
+ "type": "Ultralytics",
+ "base": "Ultralytics",
+ "save_path": "ultralytics/segm",
+ "description": "These are the available models in the UltralyticsDetectorProvider of Impact Pack.",
+ "reference": "https://github.com/hben35096/assets/releases/tag/yolo8",
+ "filename": "skin_yolov8n-seg_400.pt",
+ "url": "https://github.com/hben35096/assets/releases/download/yolo8/skin_yolov8n-seg_400.pt"
+ },
+ {
+ "name": "skin_yolov8n-seg_800.pt (segm)",
+ "type": "Ultralytics",
+ "base": "Ultralytics",
+ "save_path": "ultralytics/segm",
+ "description": "These are the available models in the UltralyticsDetectorProvider of Impact Pack.",
+ "reference": "https://github.com/hben35096/assets/releases/tag/yolo8",
+ "filename": "skin_yolov8n-seg_800.pt",
+ "url": "https://github.com/hben35096/assets/releases/download/yolo8/skin_yolov8n-seg_800.pt"
+ },
+
+ {
+ "name": "CiaraRowles/temporaldiff-v1-animatediff.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "animatediff",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/models",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/CiaraRowles/TemporalDiff",
+ "filename": "temporaldiff-v1-animatediff.ckpt",
+ "url": "https://huggingface.co/CiaraRowles/TemporalDiff/resolve/main/temporaldiff-v1-animatediff.ckpt"
+ },
+ {
+ "name": "animatediff/mm_sd_v15_v2.ckpt (ComfyUI-AnimateDiff-Evolved)",
+ "type": "animatediff",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/models",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/guoyww/animatediff",
+ "filename": "mm_sd_v15_v2.ckpt",
+ "url": "https://huggingface.co/guoyww/animatediff/resolve/main/mm_sd_v15_v2.ckpt"
+ },
+ {
+ "name": "AD_Stabilized_Motion/mm-Stabilized_high.pth (ComfyUI-AnimateDiff-Evolved)",
+ "type": "animatediff",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/models",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/manshoety/AD_Stabilized_Motion",
+ "filename": "mm-Stabilized_high.pth",
+ "url": "https://huggingface.co/manshoety/AD_Stabilized_Motion/resolve/main/mm-Stabilized_high.pth"
+ },
+ {
+ "name": "AD_Stabilized_Motion/mm-Stabilized_mid.pth (ComfyUI-AnimateDiff-Evolved)",
+ "type": "animatediff",
+ "base": "SD1.x",
+ "save_path": "custom_nodes/ComfyUI-AnimateDiff-Evolved/models",
+ "description": "Pressing 'install' directly downloads the model from the Kosinkadink/ComfyUI-AnimateDiff-Evolved extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
+ "reference": "https://huggingface.co/manshoety/AD_Stabilized_Motion",
+ "filename": "mm-Stabilized_mid.pth",
+ "url": "https://huggingface.co/manshoety/AD_Stabilized_Motion/resolve/main/mm-Stabilized_mid.pth"
+ },
+
+ {
+ "name": "GFPGANv1.4.pth",
+ "type": "GFPGAN",
+ "base": "GFPGAN",
+ "save_path": "facerestore_models",
+ "description": "Face Restoration Models. Download the model required for using the 'Facerestore CF (Code Former)' custom node.",
+ "reference": "https://github.com/TencentARC/GFPGAN/releases",
+ "filename": "GFPGANv1.4.pth",
+ "url": "https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/GFPGANv1.4.pth"
+ },
+ {
+ "name": "codeformer.pth",
+ "type": "CodeFormer",
+ "base": "CodeFormer",
+ "save_path": "facerestore_models",
+ "description": "Face Restoration Models. Download the model required for using the 'Facerestore CF (Code Former)' custom node.",
+ "reference": "https://github.com/sczhou/CodeFormer/releases",
+ "filename": "codeformer.pth",
+ "url": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth"
+ },
+ {
+ "name": "detection_Resnet50_Final.pth",
+ "type": "facexlib",
+ "base": "facexlib",
+ "save_path": "facerestore_models",
+ "description": "Face Detection Models. Download the model required for using the 'Facerestore CF (Code Former)' custom node.",
+ "reference": "https://github.com/xinntao/facexlib",
+ "filename": "detection_Resnet50_Final.pth",
+ "url": "https://github.com/xinntao/facexlib/releases/download/v0.1.0/detection_Resnet50_Final.pth"
+ },
+ {
+ "name": "detection_mobilenet0.25_Final.pth",
+ "type": "facexlib",
+ "base": "facexlib",
+ "save_path": "facerestore_models",
+ "description": "Face Detection Models. Download the model required for using the 'Facerestore CF (Code Former)' custom node.",
+ "reference": "https://github.com/xinntao/facexlib",
+ "filename": "detection_mobilenet0.25_Final.pth",
+ "url": "https://github.com/xinntao/facexlib/releases/download/v0.1.0/detection_mobilenet0.25_Final.pth"
+ },
+ {
+ "name": "yolov5l-face.pth",
+ "type": "facexlib",
+ "base": "facexlib",
+ "save_path": "facedetection",
+ "description": "Face Detection Models. Download the model required for using the 'Facerestore CF (Code Former)' custom node.",
+ "reference": "https://github.com/xinntao/facexlib",
+ "filename": "yolov5l-face.pth",
+ "url": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/yolov5l-face.pth"
+ },
+ {
+ "name": "yolov5n-face.pth",
+ "type": "facexlib",
+ "base": "facexlib",
+ "save_path": "facedetection",
+ "description": "Face Detection Models. Download the model required for using the 'Facerestore CF (Code Former)' custom node.",
+ "reference": "https://github.com/xinntao/facexlib",
+ "filename": "yolov5n-face.pth",
+ "url": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/yolov5n-face.pth"
+ },
+
+ {
+ "name": "diffusers/stable-diffusion-xl-1.0-inpainting-0.1 (UNET/fp16)",
+ "type": "unet",
+ "base": "SDXL",
+ "save_path": "unet/xl-inpaint-0.1",
+ "description": "[5.14GB] Stable Diffusion XL inpainting model 0.1. You need UNETLoader instead of CheckpointLoader.",
+ "reference": "https://huggingface.co/diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
+ "filename": "diffusion_pytorch_model.fp16.safetensors",
+ "url": "https://huggingface.co/diffusers/stable-diffusion-xl-1.0-inpainting-0.1/resolve/main/unet/diffusion_pytorch_model.fp16.safetensors"
+ },
+ {
+ "name": "diffusers/stable-diffusion-xl-1.0-inpainting-0.1 (UNET)",
+ "type": "unet",
+ "base": "SDXL",
+ "save_path": "unet/xl-inpaint-0.1",
+ "description": "[10.3GB] Stable Diffusion XL inpainting model 0.1. You need UNETLoader instead of CheckpointLoader.",
+ "reference": "https://huggingface.co/diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
+ "filename": "diffusion_pytorch_model.safetensors",
+ "url": "https://huggingface.co/diffusers/stable-diffusion-xl-1.0-inpainting-0.1/resolve/main/unet/diffusion_pytorch_model.safetensors"
+ },
+
+ {
+ "name": "Inswapper (face swap)",
+ "type": "insightface",
+ "base" : "inswapper",
+ "save_path": "insightface",
+ "description": "Checkpoint of the insightface swapper model (used by Comfy-Roop and comfy_mtb)",
+ "reference": "https://huggingface.co/deepinsight/inswapper/",
+ "filename": "inswapper_128.onnx",
+ "url": "https://huggingface.co/deepinsight/inswapper/resolve/main/inswapper_128.onnx"
+ },
+
+ {
+ "name": "CLIPVision model (stabilityai/clip_vision_g)",
+ "type": "clip_vision",
+ "base": "SDXL",
+ "save_path": "clip_vision/SDXL",
+ "description": "[3.69GB] clip_g vision model",
+ "reference": "https://huggingface.co/stabilityai/control-lora",
+ "filename": "clip_vision_g.safetensors",
+ "url": "https://huggingface.co/stabilityai/control-lora/resolve/main/revision/clip_vision_g.safetensors"
+ },
+
+ {
+ "name": "CLIPVision model (IP-Adapter)",
+ "type": "clip_vision",
+ "base": "SD1.5",
+ "save_path": "clip_vision/SD1.5",
+ "description": "[2.5GB] CLIPVision model (needed for IP-Adapter)",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "pytorch_model.bin",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/image_encoder/pytorch_model.bin"
+ },
+ {
+ "name": "CLIPVision model (IP-Adapter)",
+ "type": "clip_vision",
+ "base": "SDXL",
+ "save_path": "clip_vision/SDXL",
+ "description": "[3.69GB] CLIPVision model (needed for IP-Adapter)",
+ "reference": "https://huggingface.co/h94/IP-Adapter",
+ "filename": "pytorch_model.bin",
+ "url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/image_encoder/pytorch_model.bin"
+ },
+
+ {
+ "name": "stabilityai/control-lora-canny-rank128.safetensors",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "Control-LoRA: canny rank128",
+ "reference": "https://huggingface.co/stabilityai/control-lora",
+ "filename": "control-lora-canny-rank128.safetensors",
+ "url": "https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank128/control-lora-canny-rank128.safetensors"
+ },
+ {
+ "name": "stabilityai/control-lora-depth-rank128.safetensors",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "Control-LoRA: depth rank128",
+ "reference": "https://huggingface.co/stabilityai/control-lora",
+ "filename": "control-lora-depth-rank128.safetensors",
+ "url": "https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank128/control-lora-depth-rank128.safetensors"
+ },
+ {
+ "name": "stabilityai/control-lora-recolor-rank128.safetensors",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "Control-LoRA: recolor rank128",
+ "reference": "https://huggingface.co/stabilityai/control-lora",
+ "filename": "control-lora-recolor-rank128.safetensors",
+ "url": "https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank128/control-lora-recolor-rank128.safetensors"
+ },
+ {
+ "name": "stabilityai/control-lora-sketch-rank128-metadata.safetensors",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "Control-LoRA: sketch rank128 metadata",
+ "reference": "https://huggingface.co/stabilityai/control-lora",
+ "filename": "control-lora-sketch-rank128-metadata.safetensors",
+ "url": "https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank128/control-lora-sketch-rank128-metadata.safetensors"
+ },
+
+ {
+ "name": "stabilityai/control-lora-canny-rank256.safetensors",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "Control-LoRA: canny rank256",
+ "reference": "https://huggingface.co/stabilityai/control-lora",
+ "filename": "control-lora-canny-rank256.safetensors",
+ "url": "https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-canny-rank256.safetensors"
+ },
+ {
+ "name": "stabilityai/control-lora-depth-rank256.safetensors",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "Control-LoRA: depth rank256",
+ "reference": "https://huggingface.co/stabilityai/control-lora",
+ "filename": "control-lora-depth-rank256.safetensors",
+ "url": "https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-depth-rank256.safetensors"
+ },
+ {
+ "name": "stabilityai/control-lora-recolor-rank256.safetensors",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "Control-LoRA: recolor rank256",
+ "reference": "https://huggingface.co/stabilityai/control-lora",
+ "filename": "control-lora-recolor-rank256.safetensors",
+ "url": "https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-recolor-rank256.safetensors"
+ },
+ {
+ "name": "stabilityai/control-lora-sketch-rank256.safetensors",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "Control-LoRA: sketch rank256",
+ "reference": "https://huggingface.co/stabilityai/control-lora",
+ "filename": "control-lora-sketch-rank256.safetensors",
+ "url": "https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors"
+ },
+
+ {
+ "name": "kohya-ss/ControlNet-LLLite: SDXL Canny Anime",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "custom_nodes/ControlNet-LLLite-ComfyUI/models",
+ "description": "[46.2MB] An extremely compactly designed controlnet model (a.k.a. ControlNet-LLLite). Note: The model structure is highly experimental and may be subject to change in the future.",
+ "reference": "https://huggingface.co/kohya-ss/controlnet-lllite",
+ "filename": "controllllite_v01032064e_sdxl_canny_anime.safetensors",
+ "url": "https://huggingface.co/kohya-ss/controlnet-lllite/resolve/main/controllllite_v01032064e_sdxl_canny_anime.safetensors"
+ },
+
+ {
+ "name": "SDXL-controlnet: OpenPose (v2)",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "ControlNet openpose model for SDXL",
+ "reference": "https://huggingface.co/thibaud/controlnet-openpose-sdxl-1.0",
+ "filename": "OpenPoseXL2.safetensors",
+ "url": "https://huggingface.co/thibaud/controlnet-openpose-sdxl-1.0/resolve/main/OpenPoseXL2.safetensors"
+ },
+ {
+ "name": "controlnet-SargeZT/controlnet-sd-xl-1.0-softedge-dexined",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "ControlNet softedge model for SDXL",
+ "reference": "https://huggingface.co/SargeZT/controlnet-sd-xl-1.0-softedge-dexined",
+ "filename": "controlnet-sd-xl-1.0-softedge-dexined.safetensors",
+ "url": "https://huggingface.co/SargeZT/controlnet-sd-xl-1.0-softedge-dexined/resolve/main/controlnet-sd-xl-1.0-softedge-dexined.safetensors"
+ },
+ {
+ "name": "controlnet-SargeZT/controlnet-sd-xl-1.0-depth-16bit-zoe",
+ "type": "controlnet",
+ "base": "SDXL",
+ "save_path": "default",
+ "description": "ControlNet depth-zoe model for SDXL",
+ "reference": "https://huggingface.co/SargeZT/controlnet-sd-xl-1.0-depth-16bit-zoe",
+ "filename": "depth-zoe-xl-v1.0-controlnet.safetensors",
+ "url": "https://huggingface.co/SargeZT/controlnet-sd-xl-1.0-depth-16bit-zoe/resolve/main/depth-zoe-xl-v1.0-controlnet.safetensors"
+ }
+ ]
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/custom-node-list.json b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/custom-node-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..ee843043402ff7d1e19ca10690d46b21afd841f1
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/custom-node-list.json
@@ -0,0 +1,34 @@
+{
+ "custom_nodes": [
+ {
+ "author": "Suzie1",
+ "title": "Guide To Making Custom Nodes in ComfyUI",
+ "reference": "https://github.com/Suzie1/ComfyUI_Guide_To_Making_Custom_Nodes",
+ "files": [
+ "https://github.com/Suzie1/ComfyUI_Guide_To_Making_Custom_Nodes"
+ ],
+ "install_type": "git-clone",
+ "description": "There is a small node pack attached to this guide. This includes the init file and 3 nodes associated with the tutorials."
+ },
+ {
+ "author": "dynamixar",
+ "title": "Atluris",
+ "reference": "https://github.com/dynamixar/Atluris",
+ "files": [
+ "https://github.com/dynamixar/Atluris"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes:Random Line"
+ },
+ {
+ "author": "et118",
+ "title": "ComfyUI-ElGogh-Nodes",
+ "reference": "https://github.com/et118/ComfyUI-ElGogh-Nodes",
+ "files": [
+ "https://github.com/et118/ComfyUI-ElGogh-Nodes"
+ ],
+ "install_type": "git-clone",
+ "description": "Nodes:ElGogh Positive Prompt, ElGogh NEGATIVE Prompt, ElGogh Empty Latent Image, ElGogh Checkpoint Loader Simple"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/extension-node-map.json b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/extension-node-map.json
new file mode 100644
index 0000000000000000000000000000000000000000..9e26dfeeb6e641a33dae4961196235bdb965b21b
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/extension-node-map.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/model-list.json b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/model-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..8e3e1dc4858a08aa46190aa53ba320d565206cf4
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/model-list.json
@@ -0,0 +1,3 @@
+{
+ "models": []
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/scan.sh b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/scan.sh
new file mode 100644
index 0000000000000000000000000000000000000000..5d8d8c48b6e3f48dc1491738c1226f574909c05d
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/node_db/tutorial/scan.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+source ../../../../venv/bin/activate
+rm .tmp/*.py > /dev/null
+python ../../scanner.py
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/notebooks/comfyui_colab_with_manager.ipynb b/ComfyUI/custom_nodes/ComfyUI-Manager/notebooks/comfyui_colab_with_manager.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..36bab4fe83f42901cb136c273806537e6b6baa0d
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/notebooks/comfyui_colab_with_manager.ipynb
@@ -0,0 +1,353 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "aaaaaaaaaa"
+ },
+ "source": [
+ "Git clone the repo and install the requirements. (ignore the pip errors about protobuf)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "bbbbbbbbbb"
+ },
+ "outputs": [],
+ "source": [
+ "# #@title Environment Setup\n",
+ "\n",
+ "from pathlib import Path\n",
+ "\n",
+ "OPTIONS = {}\n",
+ "\n",
+ "USE_GOOGLE_DRIVE = True #@param {type:\"boolean\"}\n",
+ "UPDATE_COMFY_UI = True #@param {type:\"boolean\"}\n",
+ "USE_COMFYUI_MANAGER = True #@param {type:\"boolean\"}\n",
+ "INSTALL_CUSTOM_NODES_DEPENDENCIES = True #@param {type:\"boolean\"}\n",
+ "OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE\n",
+ "OPTIONS['UPDATE_COMFY_UI'] = UPDATE_COMFY_UI\n",
+ "OPTIONS['USE_COMFYUI_MANAGER'] = USE_COMFYUI_MANAGER\n",
+ "OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES'] = INSTALL_CUSTOM_NODES_DEPENDENCIES\n",
+ "\n",
+ "current_dir = !pwd\n",
+ "WORKSPACE = f\"{current_dir[0]}/ComfyUI\"\n",
+ "\n",
+ "if OPTIONS['USE_GOOGLE_DRIVE']:\n",
+ " !echo \"Mounting Google Drive...\"\n",
+ " %cd /\n",
+ "\n",
+ " from google.colab import drive\n",
+ " drive.mount('/content/drive')\n",
+ "\n",
+ " WORKSPACE = \"/content/drive/MyDrive/ComfyUI\"\n",
+ " %cd /content/drive/MyDrive\n",
+ "\n",
+ "![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n",
+ "%cd $WORKSPACE\n",
+ "\n",
+ "if OPTIONS['UPDATE_COMFY_UI']:\n",
+ " !echo -= Updating ComfyUI =-\n",
+ " !git pull\n",
+ "\n",
+ "!echo -= Install dependencies =-\n",
+ "#Remove cu121 as it causes issues in Colab.\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",
+ "!pip3 install accelerate\n",
+ "!pip3 install einops transformers>=4.25.1 safetensors>=0.3.0 aiohttp pyyaml Pillow scipy tqdm psutil\n",
+ "!pip3 install xformers!=0.0.18 torch==2.1.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121\n",
+ "!pip3 install torchsde\n",
+ "\n",
+ "if OPTIONS['USE_COMFYUI_MANAGER']:\n",
+ " %cd custom_nodes\n",
+ " ![ ! -d ComfyUI-Manager ] && echo -= Initial setup ComfyUI-Manager =- && git clone https://github.com/ltdrdata/ComfyUI-Manager\n",
+ " %cd ComfyUI-Manager\n",
+ " !git pull\n",
+ "\n",
+ "%cd $WORKSPACE\n",
+ "\n",
+ "if OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES']:\n",
+ " !pwd\n",
+ " !echo -= Install custom nodes dependencies =-\n",
+ " ![ -f \"custom_nodes/ComfyUI-Manager/scripts/colab-dependencies.py\" ] && python \"custom_nodes/ComfyUI-Manager/scripts/colab-dependencies.py\"\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "cccccccccc"
+ },
+ "source": [
+ "Download some models/checkpoints/vae or custom comfyui nodes (uncomment the commands for the ones you want)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "dddddddddd"
+ },
+ "outputs": [],
+ "source": [
+ "# Checkpoints\n",
+ "\n",
+ "### SDXL\n",
+ "### I recommend these workflow examples: https://comfyanonymous.github.io/ComfyUI_examples/sdxl/\n",
+ "\n",
+ "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "# SDXL ReVision\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors -P ./models/clip_vision/\n",
+ "\n",
+ "# SD1.5\n",
+ "!wget -c https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt -P ./models/checkpoints/\n",
+ "\n",
+ "# SD2\n",
+ "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-ema-pruned.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "# Some SD1.5 anime style\n",
+ "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1_orangemixs.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A3_orangemixs.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/Linaqruf/anything-v3.0/resolve/main/anything-v3-fp16-pruned.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "# Waifu Diffusion 1.5 (anime style SD2.x 768-v)\n",
+ "#!wget -c https://huggingface.co/waifu-diffusion/wd-1-5-beta3/resolve/main/wd-illusion-fp16.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "\n",
+ "# unCLIP models\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/illuminatiDiffusionV1_v11_unCLIP/resolve/main/illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/wd-1.5-beta2_unCLIP/resolve/main/wd-1-5-beta2-aesthetic-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "\n",
+ "# VAE\n",
+ "!wget -c https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors -P ./models/vae/\n",
+ "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt -P ./models/vae/\n",
+ "#!wget -c https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt -P ./models/vae/\n",
+ "\n",
+ "\n",
+ "# Loras\n",
+ "#!wget -c https://civitai.com/api/download/models/10350 -O ./models/loras/theovercomer8sContrastFix_sd21768.safetensors #theovercomer8sContrastFix SD2.x 768-v\n",
+ "#!wget -c https://civitai.com/api/download/models/10638 -O ./models/loras/theovercomer8sContrastFix_sd15.safetensors #theovercomer8sContrastFix SD1.x\n",
+ "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_offset_example-lora_1.0.safetensors -P ./models/loras/ #SDXL offset noise lora\n",
+ "\n",
+ "\n",
+ "# T2I-Adapter\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_seg_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_sketch_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_keypose_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_openpose_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_color_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_canny_sd14v1.pth -P ./models/controlnet/\n",
+ "\n",
+ "# T2I Styles Model\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_style_sd14v1.pth -P ./models/style_models/\n",
+ "\n",
+ "# CLIPVision model (needed for styles model)\n",
+ "#!wget -c https://huggingface.co/openai/clip-vit-large-patch14/resolve/main/pytorch_model.bin -O ./models/clip_vision/clip_vit14.bin\n",
+ "\n",
+ "\n",
+ "# ControlNet\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_ip2p_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_shuffle_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_canny_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11f1p_sd15_depth_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_inpaint_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_lineart_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_mlsd_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_normalbae_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_seg_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_softedge_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15s2_lineart_anime_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11u_sd15_tile_fp16.safetensors -P ./models/controlnet/\n",
+ "\n",
+ "# ControlNet SDXL\n",
+ "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-canny-rank256.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-depth-rank256.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-recolor-rank256.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors -P ./models/controlnet/\n",
+ "\n",
+ "# Controlnet Preprocessor nodes by Fannovel16\n",
+ "#!cd custom_nodes && git clone https://github.com/Fannovel16/comfy_controlnet_preprocessors; cd comfy_controlnet_preprocessors && python install.py\n",
+ "\n",
+ "\n",
+ "# GLIGEN\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/GLIGEN_pruned_safetensors/resolve/main/gligen_sd14_textbox_pruned_fp16.safetensors -P ./models/gligen/\n",
+ "\n",
+ "\n",
+ "# ESRGAN upscale model\n",
+ "#!wget -c https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth -P ./models/upscale_models/\n",
+ "#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x2.pth -P ./models/upscale_models/\n",
+ "#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x4.pth -P ./models/upscale_models/\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "kkkkkkkkkkkkkkk"
+ },
+ "source": [
+ "### Run ComfyUI with cloudflared (Recommended Way)\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "jjjjjjjjjjjjjj"
+ },
+ "outputs": [],
+ "source": [
+ "!wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n",
+ "!dpkg -i cloudflared-linux-amd64.deb\n",
+ "\n",
+ "import subprocess\n",
+ "import threading\n",
+ "import time\n",
+ "import socket\n",
+ "import urllib.request\n",
+ "\n",
+ "def iframe_thread(port):\n",
+ " while True:\n",
+ " time.sleep(0.5)\n",
+ " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
+ " result = sock.connect_ex(('127.0.0.1', port))\n",
+ " if result == 0:\n",
+ " break\n",
+ " sock.close()\n",
+ " print(\"\\nComfyUI finished loading, trying to launch cloudflared (if it gets stuck here cloudflared is having issues)\\n\")\n",
+ "\n",
+ " p = subprocess.Popen([\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:{}\".format(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " for line in p.stderr:\n",
+ " l = line.decode()\n",
+ " if \"trycloudflare.com \" in l:\n",
+ " print(\"This is the URL to access ComfyUI:\", l[l.find(\"http\"):], end='')\n",
+ " #print(l, end='')\n",
+ "\n",
+ "\n",
+ "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
+ "\n",
+ "!python main.py --dont-print-server"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "kkkkkkkkkkkkkk"
+ },
+ "source": [
+ "### Run ComfyUI with localtunnel\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "jjjjjjjjjjjjj"
+ },
+ "outputs": [],
+ "source": [
+ "!npm install -g localtunnel\n",
+ "\n",
+ "import subprocess\n",
+ "import threading\n",
+ "import time\n",
+ "import socket\n",
+ "import urllib.request\n",
+ "\n",
+ "def iframe_thread(port):\n",
+ " while True:\n",
+ " time.sleep(0.5)\n",
+ " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
+ " result = sock.connect_ex(('127.0.0.1', port))\n",
+ " if result == 0:\n",
+ " break\n",
+ " sock.close()\n",
+ " print(\"\\nComfyUI finished loading, trying to launch localtunnel (if it gets stuck here localtunnel is having issues)\\n\")\n",
+ "\n",
+ " print(\"The password/enpoint ip for localtunnel is:\", urllib.request.urlopen('https://ipv4.icanhazip.com').read().decode('utf8').strip(\"\\n\"))\n",
+ " p = subprocess.Popen([\"lt\", \"--port\", \"{}\".format(port)], stdout=subprocess.PIPE)\n",
+ " for line in p.stdout:\n",
+ " print(line.decode(), end='')\n",
+ "\n",
+ "\n",
+ "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
+ "\n",
+ "!python main.py --dont-print-server"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "gggggggggg"
+ },
+ "source": [
+ "### Run ComfyUI with colab iframe (use only in case the previous way with localtunnel doesn't work)\n",
+ "\n",
+ "You should see the ui appear in an iframe. If you get a 403 error, it's your firefox settings or an extension that's messing things up.\n",
+ "\n",
+ "If you want to open it in another window use the link.\n",
+ "\n",
+ "Note that some UI features like live image previews won't work because the colab iframe blocks websockets."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "hhhhhhhhhh"
+ },
+ "outputs": [],
+ "source": [
+ "import threading\n",
+ "import time\n",
+ "import socket\n",
+ "def iframe_thread(port):\n",
+ " while True:\n",
+ " time.sleep(0.5)\n",
+ " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
+ " result = sock.connect_ex(('127.0.0.1', port))\n",
+ " if result == 0:\n",
+ " break\n",
+ " sock.close()\n",
+ " from google.colab import output\n",
+ " output.serve_kernel_port_as_iframe(port, height=1024)\n",
+ " print(\"to open it in a window you can open this link here:\")\n",
+ " output.serve_kernel_port_as_window(port)\n",
+ "\n",
+ "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
+ "\n",
+ "!python main.py --dont-print-server"
+ ]
+ }
+ ],
+ "metadata": {
+ "accelerator": "GPU",
+ "colab": {
+ "provenance": []
+ },
+ "gpuClass": "standard",
+ "kernelspec": {
+ "display_name": "Python 3",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/prestartup_script.py b/ComfyUI/custom_nodes/ComfyUI-Manager/prestartup_script.py
new file mode 100644
index 0000000000000000000000000000000000000000..474744fab18630f178c7870529ac515c37b6cd7d
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/prestartup_script.py
@@ -0,0 +1,445 @@
+import datetime
+import os
+import subprocess
+import sys
+import atexit
+import threading
+import re
+import locale
+import platform
+
+
+sys.CM_api = {}
+
+
+message_collapses = []
+import_failed_extensions = set()
+
+
+def register_message_collapse(f):
+ global message_collapses
+ message_collapses.append(f)
+
+
+def is_import_failed_extension(x):
+ global import_failed_extensions
+ return x in import_failed_extensions
+
+
+sys.__comfyui_manager_register_message_collapse = register_message_collapse
+sys.__comfyui_manager_is_import_failed_extension = is_import_failed_extension
+
+comfyui_manager_path = os.path.dirname(__file__)
+custom_nodes_path = os.path.abspath(os.path.join(comfyui_manager_path, ".."))
+startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts")
+restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json")
+git_script_path = os.path.join(comfyui_manager_path, "git_helper.py")
+
+std_log_lock = threading.Lock()
+
+
+class TerminalHook:
+ def __init__(self):
+ self.hooks = {}
+
+ def add_hook(self, k, v):
+ self.hooks[k] = v
+
+ def remove_hook(self, k):
+ if k in self.hooks:
+ del self.hooks[k]
+
+ def write_stderr(self, msg):
+ for v in self.hooks.values():
+ try:
+ v.write_stderr(msg)
+ except Exception:
+ pass
+
+ def write_stdout(self, msg):
+ for v in self.hooks.values():
+ try:
+ v.write_stdout(msg)
+ except Exception:
+ pass
+
+
+terminal_hook = TerminalHook()
+sys.__comfyui_manager_terminal_hook = terminal_hook
+
+
+def handle_stream(stream, prefix):
+ stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
+ for msg in stream:
+ if prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg):
+ if msg.startswith('100%'):
+ print('\r' + msg, end="", file=sys.stderr),
+ else:
+ print('\r' + msg[:-1], end="", file=sys.stderr),
+ else:
+ if prefix == '[!]':
+ print(prefix, msg, end="", file=sys.stderr)
+ else:
+ print(prefix, msg, end="")
+
+
+def process_wrap(cmd_str, cwd_path, handler=None):
+ process = subprocess.Popen(cmd_str, cwd=cwd_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
+
+ if handler is None:
+ handler = handle_stream
+
+ stdout_thread = threading.Thread(target=handler, args=(process.stdout, ""))
+ stderr_thread = threading.Thread(target=handler, args=(process.stderr, "[!]"))
+
+ stdout_thread.start()
+ stderr_thread.start()
+
+ stdout_thread.join()
+ stderr_thread.join()
+
+ return process.wait()
+
+
+try:
+ if '--port' in sys.argv:
+ port_index = sys.argv.index('--port')
+ if port_index + 1 < len(sys.argv):
+ port = int(sys.argv[port_index + 1])
+ postfix = f"_{port}"
+ else:
+ postfix = ""
+
+ # Logger setup
+ if os.path.exists(f"comfyui{postfix}.log"):
+ if os.path.exists(f"comfyui{postfix}.prev.log"):
+ if os.path.exists(f"comfyui{postfix}.prev2.log"):
+ os.remove(f"comfyui{postfix}.prev2.log")
+ os.rename(f"comfyui{postfix}.prev.log", f"comfyui{postfix}.prev2.log")
+ os.rename(f"comfyui{postfix}.log", f"comfyui{postfix}.prev.log")
+
+ original_stdout = sys.stdout
+ original_stderr = sys.stderr
+
+ pat_tqdm = r'\d+%.*\[(.*?)\]'
+ pat_import_fail = r'seconds \(IMPORT FAILED\):'
+ pat_custom_node = r'[/\\]custom_nodes[/\\](.*)$'
+
+ is_start_mode = True
+ is_import_fail_mode = False
+
+ log_file = open(f"comfyui{postfix}.log", "w", encoding="utf-8")
+ log_lock = threading.Lock()
+
+ class ComfyUIManagerLogger:
+ def __init__(self, is_stdout):
+ self.is_stdout = is_stdout
+ self.encoding = "utf-8"
+
+ def fileno(self):
+ try:
+ if self.is_stdout:
+ return original_stdout.fileno()
+ else:
+ return original_stderr.fileno()
+ except AttributeError:
+ # Handle error
+ raise ValueError("The object does not have a fileno method")
+
+ def write(self, message):
+ global is_start_mode
+ global is_import_fail_mode
+
+ if any(f(message) for f in message_collapses):
+ return
+
+ if is_start_mode:
+ if is_import_fail_mode:
+ match = re.search(pat_custom_node, message)
+ if match:
+ import_failed_extensions.add(match.group(1))
+ is_import_fail_mode = False
+ else:
+ match = re.search(pat_import_fail, message)
+ if match:
+ is_import_fail_mode = True
+ else:
+ is_import_fail_mode = False
+
+ if 'Starting server' in message:
+ is_start_mode = False
+
+ if not self.is_stdout:
+ match = re.search(pat_tqdm, message)
+ if match:
+ message = re.sub(r'([#|])\d', r'\1▌', message)
+ message = re.sub('#', '█', message)
+ if '100%' in message:
+ self.sync_write(message)
+ else:
+ original_stderr.write(message)
+ original_stderr.flush()
+ else:
+ self.sync_write(message)
+ else:
+ self.sync_write(message)
+
+ def sync_write(self, message):
+ with log_lock:
+ log_file.write(message)
+ log_file.flush()
+
+ with std_log_lock:
+ if self.is_stdout:
+ original_stdout.write(message)
+ original_stdout.flush()
+ terminal_hook.write_stderr(message)
+ else:
+ original_stderr.write(message)
+ original_stderr.flush()
+ terminal_hook.write_stdout(message)
+
+ def flush(self):
+ log_file.flush()
+
+ with std_log_lock:
+ if self.is_stdout:
+ original_stdout.flush()
+ else:
+ original_stderr.flush()
+
+ def close(self):
+ self.flush()
+
+ def reconfigure(self, *args, **kwargs):
+ pass
+
+ # You can close through sys.stderr.close_log()
+ def close_log(self):
+ sys.stderr = original_stderr
+ sys.stdout = original_stdout
+ log_file.close()
+
+ def close_log():
+ sys.stderr = original_stderr
+ sys.stdout = original_stdout
+ log_file.close()
+
+ sys.stdout = ComfyUIManagerLogger(True)
+ sys.stderr = ComfyUIManagerLogger(False)
+
+ atexit.register(close_log)
+except Exception as e:
+ print(f"[ComfyUI-Manager] Logging failed: {e}")
+
+
+print("** ComfyUI startup time:", datetime.datetime.now())
+print("** Platform:", platform.system())
+print("** Python version:", sys.version)
+print("** Python executable:", sys.executable)
+print("** Log path:", os.path.abspath('comfyui.log'))
+
+
+def check_bypass_ssl():
+ try:
+ import configparser
+ import ssl
+ config_path = os.path.join(os.path.dirname(__file__), "config.ini")
+ config = configparser.ConfigParser()
+ config.read(config_path)
+ default_conf = config['default']
+
+ if 'bypass_ssl' in default_conf and default_conf['bypass_ssl'].lower() == 'true':
+ print(f"[ComfyUI-Manager] WARN: Unsafe - SSL verification bypass option is Enabled. (see ComfyUI-Manager/config.ini)")
+ ssl._create_default_https_context = ssl._create_unverified_context # SSL certificate error fix.
+ except Exception:
+ pass
+
+
+check_bypass_ssl()
+
+
+# Perform install
+processed_install = set()
+script_list_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "startup-scripts", "install-scripts.txt")
+pip_list = None
+
+
+def get_installed_packages():
+ global pip_list
+
+ if pip_list is None:
+ try:
+ result = subprocess.check_output([sys.executable, '-m', 'pip', 'list'], universal_newlines=True)
+ pip_list = set([line.split()[0].lower() for line in result.split('\n') if line.strip()])
+ except subprocess.CalledProcessError as e:
+ print(f"[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.")
+ return set()
+
+ return pip_list
+
+
+def is_installed(name):
+ name = name.strip()
+
+ if name.startswith('#'):
+ return True
+
+ pattern = r'([^<>!=]+)([<>!=]=?)'
+ match = re.search(pattern, name)
+
+ if match:
+ name = match.group(1)
+
+ return name.lower() in get_installed_packages()
+
+
+if os.path.exists(restore_snapshot_path):
+ try:
+ import json
+
+ cloned_repos = []
+
+ def msg_capture(stream, prefix):
+ stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
+ for msg in stream:
+ if msg.startswith("CLONE: "):
+ cloned_repos.append(msg[7:])
+ if prefix == '[!]':
+ print(prefix, msg, end="", file=sys.stderr)
+ else:
+ print(prefix, msg, end="")
+
+ elif prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg):
+ if msg.startswith('100%'):
+ print('\r' + msg, end="", file=sys.stderr),
+ else:
+ print('\r'+msg[:-1], end="", file=sys.stderr),
+ else:
+ if prefix == '[!]':
+ print(prefix, msg, end="", file=sys.stderr)
+ else:
+ print(prefix, msg, end="")
+
+ print(f"[ComfyUI-Manager] Restore snapshot.")
+ cmd_str = [sys.executable, git_script_path, '--apply-snapshot', restore_snapshot_path]
+ exit_code = process_wrap(cmd_str, custom_nodes_path, handler=msg_capture)
+
+ with open(restore_snapshot_path, 'r', encoding="UTF-8") as json_file:
+ info = json.load(json_file)
+ for url in cloned_repos:
+ try:
+ repository_name = url.split("/")[-1].strip()
+ repo_path = os.path.join(custom_nodes_path, repository_name)
+ repo_path = os.path.abspath(repo_path)
+
+ requirements_path = os.path.join(repo_path, 'requirements.txt')
+ install_script_path = os.path.join(repo_path, 'install.py')
+
+ this_exit_code = 0
+
+ if os.path.exists(requirements_path):
+ with open(requirements_path, 'r', encoding="UTF-8") as file:
+ for line in file:
+ package_name = line.strip()
+ if package_name and not is_installed(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) and f'{repo_path}/install.py' not in processed_install:
+ processed_install.add(f'{repo_path}/install.py')
+ install_cmd = [sys.executable, install_script_path]
+ print(f">>> {install_cmd} / {repo_path}")
+ this_exit_code += process_wrap(install_cmd, repo_path)
+
+ if this_exit_code != 0:
+ print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
+
+ except Exception as e:
+ print(e)
+ print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
+
+ if exit_code != 0:
+ print(f"[ComfyUI-Manager] Restore snapshot failed.")
+ else:
+ print(f"[ComfyUI-Manager] Restore snapshot done.")
+
+ except Exception as e:
+ print(e)
+ print(f"[ComfyUI-Manager] Restore snapshot failed.")
+
+ os.remove(restore_snapshot_path)
+
+
+def execute_lazy_install_script(repo_path, executable):
+ global processed_install
+
+ install_script_path = os.path.join(repo_path, "install.py")
+ requirements_path = os.path.join(repo_path, "requirements.txt")
+
+ if os.path.exists(requirements_path):
+ print(f"Install: pip packages for '{repo_path}'")
+ with open(requirements_path, "r") as requirements_file:
+ for line in requirements_file:
+ package_name = line.strip()
+ if package_name and not is_installed(package_name):
+ install_cmd = [executable, "-m", "pip", "install", package_name]
+ process_wrap(install_cmd, repo_path)
+
+ if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install:
+ processed_install.add(f'{repo_path}/install.py')
+ print(f"Install: install script for '{repo_path}'")
+ install_cmd = [executable, "install.py"]
+ process_wrap(install_cmd, repo_path)
+
+
+# Check if script_list_path exists
+if os.path.exists(script_list_path):
+ print("\n#######################################################################")
+ print("[ComfyUI-Manager] Starting dependency installation/(de)activation for the extension\n")
+
+ executed = set()
+ # Read each line from the file and convert it to a list using eval
+ with open(script_list_path, 'r', encoding="UTF-8") as file:
+ for line in file:
+ if line in executed:
+ continue
+
+ executed.add(line)
+
+ try:
+ script = eval(line)
+
+ if script[1].startswith('#') and script[1] != '#FORCE':
+ if script[1] == "#LAZY-INSTALL-SCRIPT":
+ execute_lazy_install_script(script[0], script[2])
+
+ elif os.path.exists(script[0]):
+ if script[1] == "#FORCE":
+ del script[1]
+ else:
+ if 'pip' in script[1:] and 'install' in script[1:] and is_installed(script[-1]):
+ continue
+
+ print(f"\n## ComfyUI-Manager: EXECUTE => {script[1:]}")
+ print(f"\n## Execute install/(de)activation script for '{script[0]}'")
+
+ exit_code = process_wrap(script[1:], script[0])
+
+ if exit_code != 0:
+ print(f"install/(de)activation script failed: {script[0]}")
+ else:
+ print(f"\n## ComfyUI-Manager: CANCELED => {script[1:]}")
+
+ except Exception as e:
+ print(f"[ERROR] Failed to execute install/(de)activation script: {line} / {e}")
+
+ # Remove the script_list_path file
+ if os.path.exists(script_list_path):
+ os.remove(script_list_path)
+
+ print("\n[ComfyUI-Manager] Startup script completed.")
+ print("#######################################################################\n")
+
+del processed_install
+del pip_list
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt b/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3467b3e51c131ba4bbf1db1ae8317aa45cc3ef53
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt
@@ -0,0 +1,2 @@
+GitPython
+matrix-client==0.4.0
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/scan.sh b/ComfyUI/custom_nodes/ComfyUI-Manager/scan.sh
new file mode 100644
index 0000000000000000000000000000000000000000..1b3cc3771790ebedf0c538ff3464125d52e8668c
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/scan.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+rm ~/.tmp/default/*.py > /dev/null 2>&1
+python scanner.py ~/.tmp/default
+cp extension-node-map.json node_db/new/.
+
+echo Integrity check
+./check.sh
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/scanner.py b/ComfyUI/custom_nodes/ComfyUI-Manager/scanner.py
new file mode 100644
index 0000000000000000000000000000000000000000..07809aef928d2b7e77cf731b394fd1182d2f6830
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/scanner.py
@@ -0,0 +1,311 @@
+import re
+import os
+import json
+from git import Repo
+from torchvision.datasets.utils import download_url
+import concurrent
+
+builtin_nodes = set()
+
+import sys
+
+
+# prepare temp dir
+if len(sys.argv) > 1:
+ temp_dir = sys.argv[1]
+else:
+ temp_dir = os.path.join(os.getcwd(), ".tmp")
+
+if not os.path.exists(temp_dir):
+ os.makedirs(temp_dir)
+
+print(f"TEMP DIR: {temp_dir}")
+
+
+# scan
+def scan_in_file(filename, is_builtin=False):
+ global builtin_nodes
+
+ try:
+ with open(filename, encoding='utf-8') as file:
+ code = file.read()
+ except UnicodeDecodeError:
+ with open(filename, encoding='cp949') as file:
+ code = file.read()
+
+ pattern = r"_CLASS_MAPPINGS\s*=\s*{([^}]*)}"
+ regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
+
+ nodes = set()
+ class_dict = {}
+
+ pattern2 = r'^[^=]*_CLASS_MAPPINGS\["(.*?)"\]'
+ keys = re.findall(pattern2, code)
+ for key in keys:
+ nodes.add(key.strip())
+
+ pattern3 = r'^[^=]*_CLASS_MAPPINGS\[\'(.*?)\'\]'
+ keys = re.findall(pattern3, code)
+ for key in keys:
+ nodes.add(key.strip())
+
+ matches = regex.findall(code)
+ for match in matches:
+ dict_text = match
+
+ key_value_pairs = re.findall(r"\"([^\"]*)\"\s*:\s*([^,\n]*)", dict_text)
+ for key, value in key_value_pairs:
+ class_dict[key.strip()] = value.strip()
+
+ key_value_pairs = re.findall(r"'([^']*)'\s*:\s*([^,\n]*)", dict_text)
+ for key, value in key_value_pairs:
+ class_dict[key.strip()] = value.strip()
+
+ for key, value in class_dict.items():
+ nodes.add(key.strip())
+
+ update_pattern = r"_CLASS_MAPPINGS.update\s*\({([^}]*)}\)"
+ update_match = re.search(update_pattern, code)
+ if update_match:
+ update_dict_text = update_match.group(1)
+ update_key_value_pairs = re.findall(r"\"([^\"]*)\"\s*:\s*([^,\n]*)", update_dict_text)
+ for key, value in update_key_value_pairs:
+ class_dict[key.strip()] = value.strip()
+ nodes.add(key.strip())
+
+ metadata = {}
+ lines = code.strip().split('\n')
+ for line in lines:
+ if line.startswith('@'):
+ if line.startswith("@author:") or line.startswith("@title:") or line.startswith("@nickname:") or line.startswith("@description:"):
+ key, value = line[1:].strip().split(':', 1)
+ metadata[key.strip()] = value.strip()
+
+ if is_builtin:
+ builtin_nodes += set(nodes)
+ else:
+ for x in builtin_nodes:
+ if x in nodes:
+ nodes.remove(x)
+
+ return nodes, metadata
+
+
+def get_py_file_paths(dirname):
+ file_paths = []
+
+ for root, dirs, files in os.walk(dirname):
+ if ".git" in root or "__pycache__" in root:
+ continue
+
+ for file in files:
+ if file.endswith(".py"):
+ file_path = os.path.join(root, file)
+ file_paths.append(file_path)
+
+ return file_paths
+
+
+def get_nodes(target_dir):
+ py_files = []
+ directories = []
+
+ for item in os.listdir(target_dir):
+ if ".git" in item or "__pycache__" in item:
+ continue
+
+ path = os.path.abspath(os.path.join(target_dir, item))
+
+ if os.path.isfile(path) and item.endswith(".py"):
+ py_files.append(path)
+ elif os.path.isdir(path):
+ directories.append(path)
+
+ return py_files, directories
+
+
+def get_git_urls_from_json(json_file):
+ with open(json_file, encoding='utf-8') as file:
+ data = json.load(file)
+
+ custom_nodes = data.get('custom_nodes', [])
+ git_clone_files = []
+ for node in custom_nodes:
+ if node.get('install_type') == 'git-clone':
+ files = node.get('files', [])
+ if files:
+ git_clone_files.append((files[0], node.get('title'), node.get('nodename_pattern')))
+
+ git_clone_files.append(("https://github.com/comfyanonymous/ComfyUI", "ComfyUI", None))
+
+ return git_clone_files
+
+
+def get_py_urls_from_json(json_file):
+ with open(json_file, encoding='utf-8') as file:
+ data = json.load(file)
+
+ custom_nodes = data.get('custom_nodes', [])
+ py_files = []
+ for node in custom_nodes:
+ if node.get('install_type') == 'copy':
+ files = node.get('files', [])
+ if files:
+ py_files.append((files[0], node.get('title'), node.get('nodename_pattern')))
+
+ return py_files
+
+
+def clone_or_pull_git_repository(git_url):
+ repo_name = git_url.split("/")[-1].split(".")[0]
+ repo_dir = os.path.join(temp_dir, repo_name)
+
+ if os.path.exists(repo_dir):
+ try:
+ repo = Repo(repo_dir)
+ origin = repo.remote(name="origin")
+ origin.pull(rebase=True)
+ repo.git.submodule('update', '--init', '--recursive')
+ print(f"Pulling {repo_name}...")
+ except Exception as e:
+ print(f"Pulling {repo_name} failed: {e}")
+ else:
+ try:
+ Repo.clone_from(git_url, repo_dir, recursive=True)
+ print(f"Cloning {repo_name}...")
+ except Exception as e:
+ print(f"Cloning {repo_name} failed: {e}")
+
+
+def update_custom_nodes():
+ if not os.path.exists(temp_dir):
+ os.makedirs(temp_dir)
+
+ node_info = {}
+
+ git_url_titles = get_git_urls_from_json('custom-node-list.json')
+
+ def process_git_url_title(url, title, node_pattern):
+ name = os.path.basename(url)
+ if name.endswith(".git"):
+ name = name[:-4]
+
+ node_info[name] = (url, title, node_pattern)
+ clone_or_pull_git_repository(url)
+
+ with concurrent.futures.ThreadPoolExecutor(10) as executor:
+ for url, title, node_pattern in git_url_titles:
+ executor.submit(process_git_url_title, url, title, node_pattern)
+
+ py_url_titles_and_pattern = get_py_urls_from_json('custom-node-list.json')
+
+ def download_and_store_info(url_title_and_pattern):
+ url, title, node_pattern = url_title_and_pattern
+ name = os.path.basename(url)
+ if name.endswith(".py"):
+ node_info[name] = (url, title, node_pattern)
+
+ try:
+ download_url(url, temp_dir)
+ except:
+ print(f"[ERROR] Cannot download '{url}'")
+
+ with concurrent.futures.ThreadPoolExecutor(10) as executor:
+ executor.map(download_and_store_info, py_url_titles_and_pattern)
+
+ return node_info
+
+
+def gen_json(node_info):
+ # scan from .py file
+ node_files, node_dirs = get_nodes(temp_dir)
+
+ comfyui_path = os.path.abspath(os.path.join(temp_dir, "ComfyUI"))
+ node_dirs.remove(comfyui_path)
+ node_dirs = [comfyui_path] + node_dirs
+
+ data = {}
+ for dirname in node_dirs:
+ py_files = get_py_file_paths(dirname)
+ metadata = {}
+
+ nodes = set()
+ for py in py_files:
+ nodes_in_file, metadata_in_file = scan_in_file(py, dirname == "ComfyUI")
+ nodes.update(nodes_in_file)
+ metadata.update(metadata_in_file)
+
+ dirname = os.path.basename(dirname)
+
+ if len(nodes) > 0 or (dirname in node_info and node_info[dirname][2] is not None):
+ nodes = list(nodes)
+ nodes.sort()
+
+ if dirname in node_info:
+ git_url, title, node_pattern = node_info[dirname]
+ metadata['title_aux'] = title
+ if node_pattern is not None:
+ metadata['nodename_pattern'] = node_pattern
+ data[git_url] = (nodes, metadata)
+ else:
+ print(f"WARN: {dirname} is removed from custom-node-list.json")
+
+ for file in node_files:
+ nodes, metadata = scan_in_file(file)
+
+ if len(nodes) > 0 or (dirname in node_info and node_info[dirname][2] is not None):
+ nodes = list(nodes)
+ nodes.sort()
+
+ file = os.path.basename(file)
+
+ if file in node_info:
+ url, title, node_pattern = node_info[file]
+ metadata['title_aux'] = title
+ if node_pattern is not None:
+ metadata['nodename_pattern'] = node_pattern
+ data[url] = (nodes, metadata)
+ else:
+ print(f"Missing info: {file}")
+
+ # scan from node_list.json file
+ extensions = [name for name in os.listdir(temp_dir) if os.path.isdir(os.path.join(temp_dir, name))]
+
+ for extension in extensions:
+ node_list_json_path = os.path.join(temp_dir, extension, 'node_list.json')
+ if os.path.exists(node_list_json_path):
+ git_url, title, node_pattern = node_info[extension]
+
+ with open(node_list_json_path, 'r', encoding='utf-8') as f:
+ node_list_json = json.load(f)
+
+ metadata_in_url = {}
+ if git_url not in data:
+ nodes = set()
+ else:
+ nodes_in_url, metadata_in_url = data[git_url]
+ nodes = set(nodes_in_url)
+
+ for x, desc in node_list_json.items():
+ nodes.add(x.strip())
+
+ metadata_in_url['title_aux'] = title
+ if node_pattern is not None:
+ metadata_in_url['nodename_pattern'] = node_pattern
+ nodes = list(nodes)
+ nodes.sort()
+ data[git_url] = (nodes, metadata_in_url)
+
+ json_path = f"extension-node-map.json"
+ with open(json_path, "w", encoding='utf-8') as file:
+ json.dump(data, file, indent=4, sort_keys=True)
+
+
+print("### ComfyUI Manager Node Scanner ###")
+
+print("\n# Updating extensions\n")
+updated_node_info = update_custom_nodes()
+
+print("\n# 'extension-node-map.json' file is generated.\n")
+gen_json(updated_node_info)
+
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/colab-dependencies.py b/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/colab-dependencies.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5a70ed6dd92ba90e8084e07fbb9097fe3096ea5
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/colab-dependencies.py
@@ -0,0 +1,39 @@
+import os
+import subprocess
+
+
+def get_enabled_subdirectories_with_files(base_directory):
+ subdirs_with_files = []
+ for subdir in os.listdir(base_directory):
+ try:
+ full_path = os.path.join(base_directory, subdir)
+ if os.path.isdir(full_path) and not subdir.endswith(".disabled") and not subdir.startswith('.') and subdir != '__pycache__':
+ print(f"## Install dependencies for '{subdir}'")
+ requirements_file = os.path.join(full_path, "requirements.txt")
+ install_script = os.path.join(full_path, "install.py")
+
+ if os.path.exists(requirements_file) or os.path.exists(install_script):
+ subdirs_with_files.append((full_path, requirements_file, install_script))
+ except Exception as e:
+ print(f"EXCEPTION During Dependencies INSTALL on '{subdir}':\n{e}")
+
+ return subdirs_with_files
+
+
+def install_requirements(requirements_file_path):
+ if os.path.exists(requirements_file_path):
+ subprocess.run(["pip", "install", "-r", requirements_file_path])
+
+
+def run_install_script(install_script_path):
+ if os.path.exists(install_script_path):
+ subprocess.run(["python", install_script_path])
+
+
+custom_nodes_directory = "custom_nodes"
+subdirs_with_files = get_enabled_subdirectories_with_files(custom_nodes_directory)
+
+
+for subdir, requirements_file, install_script in subdirs_with_files:
+ install_requirements(requirements_file)
+ run_install_script(install_script)
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh b/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh
new file mode 100644
index 0000000000000000000000000000000000000000..be473dc66f8eeb36c48d409945eb5ae83a030171
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh
@@ -0,0 +1,21 @@
+git clone https://github.com/comfyanonymous/ComfyUI
+cd ComfyUI/custom_nodes
+git clone https://github.com/ltdrdata/ComfyUI-Manager
+cd ..
+python -m venv venv
+source venv/bin/activate
+python -m pip install -r requirements.txt
+python -m pip install -r custom_nodes/ComfyUI-Manager/requirements.txt
+python -m pip install torchvision
+cd ..
+echo "#!/bin/bash" > run_gpu.sh
+echo "cd ComfyUI" >> run_gpu.sh
+echo "source venv/bin/activate" >> run_gpu.sh
+echo "python main.py --preview-method auto" >> run_gpu.sh
+chmod +x run_gpu.sh
+
+echo "#!/bin/bash" > run_cpu.sh
+echo "cd ComfyUI" >> run_cpu.sh
+echo "source venv/bin/activate" >> run_cpu.sh
+echo "python main.py --preview-method auto --cpu" >> run_cpu.sh
+chmod +x run_cpu.sh
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/install-comfyui-venv-win.bat b/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/install-comfyui-venv-win.bat
new file mode 100644
index 0000000000000000000000000000000000000000..6bb0e8364b5170530c2a85341ad754764c6788ae
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/install-comfyui-venv-win.bat
@@ -0,0 +1,20 @@
+git clone https://github.com/comfyanonymous/ComfyUI
+cd ComfyUI/custom_nodes
+git clone https://github.com/ltdrdata/ComfyUI-Manager
+cd ..
+python -m venv venv
+call venv/Scripts/activate
+python -m pip install -r requirements.txt
+python -m pip install -r custom_nodes/ComfyUI-Manager/requirements.txt
+python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu118 xformers
+cd ..
+echo "cd ComfyUI" >> run_gpu.sh
+echo "call venv/Scripts/activate" >> run_gpu.sh
+echo "python main.py" >> run_gpu.sh
+chmod +x run_gpu.sh
+
+echo "#!/bin/bash" > run_cpu.sh
+echo "cd ComfyUI" >> run_cpu.sh
+echo "call venv/Scripts/activate" >> run_cpu.sh
+echo "python main.py --cpu" >> run_cpu.sh
+chmod +x run_cpu.sh
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/install-manager-for-portable-version.bat b/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/install-manager-for-portable-version.bat
new file mode 100644
index 0000000000000000000000000000000000000000..7b067dfd770d197ccd68e760087536552223f260
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/install-manager-for-portable-version.bat
@@ -0,0 +1,2 @@
+.\python_embeded\python.exe -s -m pip install gitpython
+.\python_embeded\python.exe -c "import git; git.Repo.clone_from('https://github.com/ltdrdata/ComfyUI-Manager', './ComfyUI/custom_nodes/ComfyUI-Manager')"
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/update-fix.py b/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/update-fix.py
new file mode 100644
index 0000000000000000000000000000000000000000..d2ac10074607544d0b9cdaf4372e43c7f62bb8d0
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-Manager/scripts/update-fix.py
@@ -0,0 +1,12 @@
+import git
+
+commit_hash = "a361cc1"
+
+repo = git.Repo('.')
+
+if repo.is_dirty():
+ repo.git.stash()
+
+repo.git.update_ref("refs/remotes/origin/main", commit_hash)
+repo.remotes.origin.fetch()
+repo.git.pull("origin", "main")
diff --git a/ComfyUI/custom_nodes/ComfyUI-Manager/snapshots/the_snapshot_files_are_located_here b/ComfyUI/custom_nodes/ComfyUI-Manager/snapshots/the_snapshot_files_are_located_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/.gitignore b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..68bc17f9ff2104a9d7b6777058bb4c343ca72609
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/.gitignore
@@ -0,0 +1,160 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/LICENSE b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..f288702d2fa16d3cdf0035b15a9fcbc552cd88e7
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/README.md b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..3b21911a54cf6f006186fc351edef71a643f0a9a
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/README.md
@@ -0,0 +1,110 @@
+# ComfyUI-VideoHelperSuite
+Nodes related to video workflows
+
+## I/O Nodes
+### Load Video
+Converts a video file into a series of images
+- video: The video file to be loaded
+- force_rate: Discards or duplicates frames as needed to hit a target frame rate. Disabled by setting to 0. This can be used to quickly match a suggested frame rate like the 8 fps of AnimateDiff.
+- force_size: Allows for quick resizing to a number of suggested sizes. Several options allow you to set only width or height and determine the other from aspect ratio.
+- frame_load_cap: The maximum number of frames which will be returned. This could also be thought of as the maximum batch size.
+- skip_first_frames: How many frames to skip from the start of the video after adjusting for a forced frame rate. By incrementing this number by the frame_load_cap, you can easily process a longer input video in parts.
+- select_every_nth: Allows for skipping a number of frames without considering the base frame rate or risking frame duplication. Often useful when working with animated gifs
+A path variant of the Load Video node exists that allows loading videos from external paths
+
+
+If [Advanced Previews](#advanced-previews) is enabled in the options menu of the web ui, the preview will reflect the current settings on the node.
+### Load Image Sequence
+Loads all image files from a subfolder. Options are similar to Load Video.
+- image_load_cap: The maximum number of images which will be returned. This could also be thought of as the maximum batch size.
+- skip_first_images: How many images to skip. By incrementing this number by image_load_cap, you can easily divide a long sequence of images into multiple batches.
+- select_every_nth: Allows for skipping a number of images between every returned frame.
+
+A path variant of Load Image sequence also exists.
+### Video Combine
+Combines a series of images into an output video
+If the optional audio input is provided, it will also be combined into the output video
+- frame_rate: How many of the input frames are displayed per second. A higher frame rate means that the output video plays faster and has less duration. This should usually be kept to 8 for AnimateDiff, or matched to the force_rate of a Load Video node.
+- loop_count: How many additional times the video should repeat
+- filename_prefix: The base file name used for output.
+ - You can save output to a subfolder: `subfolder/video`
+ - Like the builtin Save Image node, you can add timestamps. `%date:yyyy-MM-ddThh:mm:ss%` might become 2023-10-31T6:45:25
+- format: The file format to use. Advanced information on configuring or adding additional video formats can be found in the [Video Formats](#video-formats) section.
+- pingpong: Causes the input to be played back in the reverse to create a clean loop.
+- save_output: Whether the image should be put into the output directory or the temp directory.
+Returns: a `VHS_FILENAMES` which consists of a boolean indicating if save_output is enabled and a list of the full filepaths of all generated outputs in the order created. Accordingly `output[1][-1]` will be the most complete output.
+
+Depending on the format chosen, additional options may become available, including
+- crf: Describes the quality of the output video. A lower number gives a higher quality video and a larger file size, while a higher number gives a lower quality video with a smaller size. Scaling varies by codec, but visually lossless output generally occurs around 20.
+- save_metadata: Includes a copy of the workflow in the ouput video which can be loaded by dragging and dropping the video, just like with images.
+- pix_fmt: Changes how the pixel data is stored. `yuv420p10le` has higher color quality, but won't work on all devices
+### Load Audio
+Provides a way to load standalone audio files.
+- seek_seconds: An optional start time for the audio file in seconds.
+
+## Latent/Image Nodes
+A number of utility nodes exist for managing latents. For each, there is an equivalent node which works on images.
+### Split Batch
+Divides the latents into two sets. The first `split_index` latents go to ouput A and the remainder to output B. If less then `split_index` latents are provided as input, all are passed to output A and output B is empty.
+### Merge Batch
+Combines two groups of latents into a single output. The order of the output is the latents in A followed by the latents in B.
+If the input groups are not the same size, the node provides options for rescaling the latents before merging.
+### Select Every Nth
+The first of every `select_every_nth` input is passed and the remainder are discarded
+### Get Count
+### Duplicate Batch
+
+## Video Previews
+Load Video (Upload), Load Video (Path), Load Images (Upload), Load Images (Path) and Video Combine provide animated previews.
+Nodes with previews provide additional functionality when right clicked
+- Open preview
+- Save preview
+- Pause preview: Can improve performance with very large videos
+- Hide preview: Can improve performance, save space
+- Sync preview: Restarts all previews for side-by-side comparisons
+
+### Advanced Previews
+Advanced Previews must be manually enabled by clicking the settings gear next to Queue Prompt and checking the box for VHS Advanced Previews.
+If enabled, videos which are displayed in the ui will be converted with ffmpeg on request. This has several benefits
+- Previews for Load Video nodes will reflect the settings on the node such as skip_first_frames and frame_load_cap
+ - This makes it easy to select an exact portion of an input video and sync it with outputs
+- It can use substantially less bandwidth if running the server remotely
+- It can greatly improve the browser performance by downsizing videos to the in ui resolution, particularly useful with animated gifs
+- It allows for previews of videos that would not normally be playable in browser.
+- Can be limited to subdirectories of ComyUI if `VHS_STRICT_PATHS` is set as an environment variable.
+
+This fucntionality is disabled since it comes with several downsides
+- There is a delay before videos show in the browser. This delay can become quite large if the input video is long
+- The preview videos are lower quality (The original can always be viewed with Right Click -> Open preview)
+
+## Video Formats
+Those familiar with ffmpeg are able to add json files to the video_formats folders to add new output types to Video Combine.
+Consider the following example for av1-webm
+```json
+{
+ "main_pass":
+ [
+ "-n", "-c:v", "libsvtav1",
+ "-pix_fmt", "yuv420p10le",
+ "-crf", ["crf","INT", {"default": 23, "min": 0, "max": 100, "step": 1}]
+ ],
+ "audio_pass": ["-c:a", "libopus"],
+ "extension": "webm",
+ "environment": {"SVT_LOG": "1"}
+}
+```
+Most configuration takes place in `main_pass`, which is a list of arguments that are passed to ffmpeg.
+- `"-n"` designates that the command should fail if a file of the same name already exists. This should never happen, but if some bug were to occur, it would ensure other files aren't overwritten.
+- `"-c:v", "libsvtav1"` designates that the video should be encoded with an av1 codec using the new SVT-AV1 encoder. SVT-AV1 is much faster than libaom-av1, but may not exist in older versions of ffmpeg. Alternatively, av1_nvenc could be used for gpu encoding with newer nvidia cards.
+- `"-pix_fmt", "yuv420p10le"` designates the standard pixel format with 10-bit color. It's important that some pixel format be specified to ensure a nonconfigurable input pix_fmt isn't used.
+
+`audio pass` contains a list of arguments which are passed to ffmpeg when audio is passed into Video Combine
+
+`extension` designates both the file extension and the container format that is used. If some of the above options are omitted from `main_pass` it can affect what default options are chosen.
+`environment` can optionally be provided to set environment variables during execution. For av1 it's used to reduce the verbosity of logging so that only major errors are displayed.
+`input_color_depth` effects the format in which pixels are passed to the ffmpeg subprocess. Current valid options are `8bit` and `16bit`. The later will produce higher quality output, but is experimental.
+
+Fields can be exposed in the webui as a widget using a format similar to what is used in the creation of custom nodes. In the above example, the argument for `-crf` will be exposed as a format widget in the webui. Format widgets are a list of up to 3 terms
+- The name of the widget that will be displayed in the web ui
+- Either a primitive such as "INT" or "BOOLEAN", or a list of string options
+- A dictionary of options
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/__init__.py b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..cae39593a7307fe8dd8a9055e643fd572fb988e8
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/__init__.py
@@ -0,0 +1,6 @@
+from .videohelpersuite.nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
+import folder_paths
+from .videohelpersuite.server import server
+
+WEB_DIRECTORY = "./web"
+__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/__pycache__/__init__.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b53fdc72732fb5fa9adf9143d77eb29e8c5c8f11
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/__pycache__/__init__.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/requirements.txt b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4fa34aa21b85c4b974e2a2b6891eae5fd6dd4164
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/requirements.txt
@@ -0,0 +1,2 @@
+opencv-python
+imageio-ffmpeg
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/ProRes.json b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/ProRes.json
new file mode 100644
index 0000000000000000000000000000000000000000..84ff1fe38e9aa610c98b99b5987b34132fd21e5c
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/ProRes.json
@@ -0,0 +1,10 @@
+{
+ "main_pass":
+ [
+ "-n", "-c:v", "prores_ks",
+ "-profile:v","3",
+ "-pix_fmt", "yuv422p10"
+ ],
+ "audio_pass": ["-c:a", "pcm_s16le"],
+ "extension": "mov"
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/av1-webm.json b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/av1-webm.json
new file mode 100644
index 0000000000000000000000000000000000000000..ceb53b4dae01df7a016a8859e2ccc53d9d849038
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/av1-webm.json
@@ -0,0 +1,13 @@
+{
+ "main_pass":
+ [
+ "-n", "-c:v", "libsvtav1",
+ "-pix_fmt", ["pix_fmt", ["yuv420p10le", "yuv420p"]],
+ "-crf", ["crf","INT", {"default": 23, "min": 0, "max": 100, "step": 1}]
+ ],
+ "audio_pass": ["-c:a", "libopus"],
+ "input_color_depth": ["input_color_depth", ["8bit", "16bit"]],
+ "save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
+ "extension": "webm",
+ "environment": {"SVT_LOG": "1"}
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/h264-mp4.json b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/h264-mp4.json
new file mode 100644
index 0000000000000000000000000000000000000000..c860f921c32231996a6fe7355172e65a214b00ad
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/h264-mp4.json
@@ -0,0 +1,11 @@
+{
+ "main_pass":
+ [
+ "-n", "-c:v", "libx264",
+ "-pix_fmt", ["pix_fmt", ["yuv420p", "yuv420p10le"]],
+ "-crf", ["crf","INT", {"default": 19, "min": 0, "max": 100, "step": 1}]
+ ],
+ "audio_pass": ["-c:a", "aac"],
+ "save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
+ "extension": "mp4"
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/h265-mp4.json b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/h265-mp4.json
new file mode 100644
index 0000000000000000000000000000000000000000..6f77fe29daa4de82e9e3d6fe385e673cf63b0cad
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/h265-mp4.json
@@ -0,0 +1,13 @@
+{
+ "main_pass":
+ [
+ "-n", "-c:v", "libx265",
+ "-pix_fmt", ["pix_fmt", ["yuv420p10le", "yuv420p"]],
+ "-crf", ["crf","INT", {"default": 22, "min": 0, "max": 100, "step": 1}],
+ "-preset", "medium",
+ "-x265-params", "log-level=quiet"
+ ],
+ "audio_pass": ["-c:a", "aac"],
+ "save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
+ "extension": "mp4"
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/webm.json b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/webm.json
new file mode 100644
index 0000000000000000000000000000000000000000..66eacb1144704d05680dab38d59d399f966bc723
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/video_formats/webm.json
@@ -0,0 +1,12 @@
+{
+ "main_pass":
+ [
+ "-n",
+ "-pix_fmt", "yuv420p",
+ "-crf", ["crf","INT", {"default": 20, "min": 0, "max": 100, "step": 1}],
+ "-b:v", "0"
+ ],
+ "audio_pass": ["-c:a", "libvorbis"],
+ "save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
+ "extension": "webm"
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/image_latent_nodes.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/image_latent_nodes.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3b6efe3aada4714c8d71a8a708704957b23a25b2
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/image_latent_nodes.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/load_images_nodes.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/load_images_nodes.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..00d9257aca002420f3690a3d61fef9adbfc56167
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/load_images_nodes.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/load_video_nodes.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/load_video_nodes.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..46ceb67f31fdce43fa21c2c9e1f8619f79fd4884
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/load_video_nodes.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/logger.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/logger.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..fee9efbf26e0155cb565fd7c03c80b353757866e
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/logger.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/nodes.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/nodes.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8aba66c5ea235dc3f8ced9d7fc9984b433cd0d49
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/nodes.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/server.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/server.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a8d227593ec8b0e8668c6b5412f0abc35caeb57b
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/server.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/utils.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/utils.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..653f9ef9233fbe3e7b391f769c7fc85e01b82f44
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/__pycache__/utils.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/image_latent_nodes.py b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/image_latent_nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..988f6954b9eeaed076dd272584cebed45418f392
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/image_latent_nodes.py
@@ -0,0 +1,318 @@
+from torch import Tensor
+import torch
+
+import comfy.utils
+
+
+class MergeStrategies:
+ MATCH_A = "match A"
+ MATCH_B = "match B"
+ MATCH_SMALLER = "match smaller"
+ MATCH_LARGER = "match larger"
+
+ list_all = [MATCH_A, MATCH_B, MATCH_SMALLER, MATCH_LARGER]
+
+
+class ScaleMethods:
+ NEAREST_EXACT = "nearest-exact"
+ BILINEAR = "bilinear"
+ AREA = "area"
+ BICUBIC = "bicubic"
+ BISLERP = "bislerp"
+
+ list_all = [NEAREST_EXACT, BILINEAR, AREA, BICUBIC, BISLERP]
+
+
+class CropMethods:
+ DISABLED = "disabled"
+ CENTER = "center"
+
+ list_all = [DISABLED, CENTER]
+
+
+class SplitLatents:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "latents": ("LATENT",),
+ "split_index": ("INT", {"default": 0, "step": 1, "min": -99999999999}),
+ },
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/latent"
+
+ RETURN_TYPES = ("LATENT", "INT", "LATENT", "INT")
+ RETURN_NAMES = ("LATENT_A", "A_count", "LATENT_B", "B_count")
+ FUNCTION = "split_latents"
+
+ def split_latents(self, latents: dict, split_index: int):
+ latents = latents.copy()
+ group_a = latents["samples"][:split_index]
+ group_b = latents["samples"][split_index:]
+ group_a_latent = {"samples": group_a}
+ group_b_latent = {"samples": group_b}
+ return (group_a_latent, group_a.size(0), group_b_latent, group_b.size(0))
+
+
+class SplitImages:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "images": ("IMAGE",),
+ "split_index": ("INT", {"default": 0, "step": 1, "min": -99999999999}),
+ },
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/image"
+
+ RETURN_TYPES = ("IMAGE", "INT", "IMAGE", "INT")
+ RETURN_NAMES = ("IMAGE_A", "A_count", "IMAGE_B", "B_count")
+ FUNCTION = "split_images"
+
+ def split_images(self, images: Tensor, split_index: int):
+ group_a = images[:split_index]
+ group_b = images[split_index:]
+ return (group_a, group_a.size(0), group_b, group_b.size(0))
+
+
+class MergeLatents:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "latents_A": ("LATENT",),
+ "latents_B": ("LATENT",),
+ "merge_strategy": (MergeStrategies.list_all,),
+ "scale_method": (ScaleMethods.list_all,),
+ "crop": (CropMethods.list_all,),
+ }
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/latent"
+
+ RETURN_TYPES = ("LATENT", "INT",)
+ RETURN_NAMES = ("LATENT", "count",)
+ FUNCTION = "merge"
+
+ def merge(self, latents_A: dict, latents_B: dict, merge_strategy: str, scale_method: str, crop: str):
+ latents = []
+ latents_A = latents_A.copy()["samples"]
+ latents_B = latents_B.copy()["samples"]
+
+ # if not same dimensions, do scaling
+ if latents_A.shape[3] != latents_B.shape[3] or latents_A.shape[2] != latents_B.shape[2]:
+ A_size = latents_A.shape[3] * latents_A.shape[2]
+ B_size = latents_B.shape[3] * latents_B.shape[2]
+ # determine which to use
+ use_A_as_template = True
+ if merge_strategy == MergeStrategies.MATCH_A:
+ pass
+ elif merge_strategy == MergeStrategies.MATCH_B:
+ use_A_as_template = False
+ elif merge_strategy in (MergeStrategies.MATCH_SMALLER, MergeStrategies.MATCH_LARGER):
+ if A_size <= B_size:
+ use_A_as_template = True if merge_strategy == MergeStrategies.MATCH_SMALLER else False
+ # apply scaling
+ if use_A_as_template:
+ latents_B = comfy.utils.common_upscale(latents_B, latents_A.shape[3], latents_A.shape[2], scale_method, crop)
+ else:
+ latents_A = comfy.utils.common_upscale(latents_A, latents_B.shape[3], latents_B.shape[2], scale_method, crop)
+
+ latents.append(latents_A)
+ latents.append(latents_B)
+
+ merged = {"samples": torch.cat(latents, dim=0)}
+ return (merged, len(merged["samples"]),)
+
+
+class MergeImages:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "images_A": ("IMAGE",),
+ "images_B": ("IMAGE",),
+ "merge_strategy": (MergeStrategies.list_all,),
+ "scale_method": (ScaleMethods.list_all,),
+ "crop": (CropMethods.list_all,),
+ }
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/image"
+
+ RETURN_TYPES = ("IMAGE", "INT",)
+ RETURN_NAMES = ("IMAGE", "count",)
+ FUNCTION = "merge"
+
+ def merge(self, images_A: Tensor, images_B: Tensor, merge_strategy: str, scale_method: str, crop: str):
+ images = []
+ # if not same dimensions, do scaling
+ if images_A.shape[3] != images_B.shape[3] or images_A.shape[2] != images_B.shape[2]:
+ images_A = images_A.movedim(-1,1)
+ images_B = images_B.movedim(-1,1)
+
+ A_size = images_A.shape[3] * images_A.shape[2]
+ B_size = images_B.shape[3] * images_B.shape[2]
+ # determine which to use
+ use_A_as_template = True
+ if merge_strategy == MergeStrategies.MATCH_A:
+ pass
+ elif merge_strategy == MergeStrategies.MATCH_B:
+ use_A_as_template = False
+ elif merge_strategy in (MergeStrategies.MATCH_SMALLER, MergeStrategies.MATCH_LARGER):
+ if A_size <= B_size:
+ use_A_as_template = True if merge_strategy == MergeStrategies.MATCH_SMALLER else False
+ # apply scaling
+ if use_A_as_template:
+ images_B = comfy.utils.common_upscale(images_B, images_A.shape[3], images_A.shape[2], scale_method, crop)
+ else:
+ images_A = comfy.utils.common_upscale(images_A, images_B.shape[3], images_B.shape[2], scale_method, crop)
+ images_A = images_A.movedim(1,-1)
+ images_B = images_B.movedim(1,-1)
+
+ images.append(images_A)
+ images.append(images_B)
+ all_images = torch.cat(images, dim=0)
+ return (all_images, all_images.size(0),)
+
+
+class SelectEveryNthLatent:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "latents": ("LATENT",),
+ "select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
+ },
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/latent"
+
+ RETURN_TYPES = ("LATENT", "INT",)
+ RETURN_NAMES = ("LATENT", "count",)
+ FUNCTION = "select_latents"
+
+ def select_latents(self, latents: dict, select_every_nth: int):
+ sub_latents = latents.copy()["samples"][0::select_every_nth]
+ return ({"samples": sub_latents}, sub_latents.size(0))
+
+
+class SelectEveryNthImage:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "images": ("IMAGE",),
+ "select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
+ },
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/image"
+
+ RETURN_TYPES = ("IMAGE", "INT",)
+ RETURN_NAMES = ("IMAGE", "count",)
+ FUNCTION = "select_images"
+
+ def select_images(self, images: Tensor, select_every_nth: int):
+ sub_images = images[0::select_every_nth]
+ return (sub_images, sub_images.size(0))
+
+
+class GetLatentCount:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "latents": ("LATENT",),
+ }
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/latent"
+
+ RETURN_TYPES = ("INT",)
+ RETURN_NAMES = ("count",)
+ FUNCTION = "count_input"
+
+ def count_input(self, latents: dict):
+ return (latents["samples"].size(0),)
+
+
+class GetImageCount:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "images": ("IMAGE",),
+ }
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/image"
+
+ RETURN_TYPES = ("INT",)
+ RETURN_NAMES = ("count",)
+ FUNCTION = "count_input"
+
+ def count_input(self, images: Tensor):
+ return (images.size(0),)
+
+
+class DuplicateLatents:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "latents": ("LATENT",),
+ "multiply_by": ("INT", {"default": 1, "min": 1, "step": 1})
+ }
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/latent"
+
+ RETURN_TYPES = ("LATENT", "INT",)
+ RETURN_NAMES = ("LATENT", "count",)
+ FUNCTION = "duplicate_input"
+
+ def duplicate_input(self, latents: dict[str, Tensor], multiply_by: int):
+ new_latents = latents.copy()
+ full_latents = []
+ for n in range(0, multiply_by):
+ full_latents.append(new_latents["samples"])
+ new_latents["samples"] = torch.cat(full_latents, dim=0)
+ return (new_latents, new_latents["samples"].size(0),)
+
+
+class DuplicateImages:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "images": ("IMAGE",),
+ "multiply_by": ("INT", {"default": 1, "min": 1, "step": 1})
+ }
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/image"
+
+ RETURN_TYPES = ("IMAGE", "INT",)
+ RETURN_NAMES = ("IMAGE", "count",)
+ FUNCTION = "duplicate_input"
+
+ def duplicate_input(self, images: Tensor, multiply_by: int):
+ full_images = []
+ for n in range(0, multiply_by):
+ full_images.append(images)
+ new_images = torch.cat(full_images, dim=0)
+ return (new_images, new_images.size(0),)
+
+
+# class SelectLatents:
+# @classmethod
+# def INPUT_TYPES(s):
+# return {
+# "required": {
+# "images": ("IMAGE",),
+# "select_indeces": ("STRING", {"default": ""}),
+# },
+# }
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/load_images_nodes.py b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/load_images_nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..bc08da8fc3fbb9f4e6cde152cf72b74135a25937
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/load_images_nodes.py
@@ -0,0 +1,157 @@
+import os
+import hashlib
+import numpy as np
+import torch
+from PIL import Image, ImageOps
+
+import folder_paths
+from comfy.k_diffusion.utils import FolderOfImages
+from .logger import logger
+from .utils import calculate_file_hash, get_sorted_dir_files_from_directory, validate_path
+
+
+def is_changed_load_images(directory: str, image_load_cap: int = 0, skip_first_images: int = 0, select_every_nth: int = 1):
+ if not os.path.isdir(directory):
+ return False
+
+ dir_files = get_sorted_dir_files_from_directory(directory, skip_first_images, select_every_nth, FolderOfImages.IMG_EXTENSIONS)
+ if image_load_cap != 0:
+ dir_files = dir_files[:image_load_cap]
+
+ m = hashlib.sha256()
+ for filepath in dir_files:
+ m.update(calculate_file_hash(filepath).encode()) # strings must be encoded before hashing
+ return m.digest().hex()
+
+
+def validate_load_images(directory: str):
+ if not os.path.isdir(directory):
+ return f"Directory '{directory}' cannot be found."
+ dir_files = os.listdir(directory)
+ if len(dir_files) == 0:
+ return f"No files in directory '{directory}'."
+
+ return True
+
+
+def load_images(directory: str, image_load_cap: int = 0, skip_first_images: int = 0, select_every_nth: int = 1):
+ if not os.path.isdir(directory):
+ raise FileNotFoundError(f"Directory '{directory} cannot be found.")
+
+ dir_files = get_sorted_dir_files_from_directory(directory, skip_first_images, select_every_nth, FolderOfImages.IMG_EXTENSIONS)
+
+ if len(dir_files) == 0:
+ raise FileNotFoundError(f"No files in directory '{directory}'.")
+
+ images = []
+ masks = []
+
+ limit_images = False
+ if image_load_cap > 0:
+ limit_images = True
+ image_count = 0
+ loaded_alpha = False
+ zero_mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
+
+ for image_path in dir_files:
+ if limit_images and image_count >= image_load_cap:
+ break
+ i = Image.open(image_path)
+ i = ImageOps.exif_transpose(i)
+ image = i.convert("RGB")
+ image = np.array(image).astype(np.float32) / 255.0
+ image = torch.from_numpy(image)[None,]
+ if 'A' in i.getbands():
+ mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
+ mask = 1. - torch.from_numpy(mask)
+ if not loaded_alpha:
+ loaded_alpha = True
+ zero_mask = torch.zeros((len(image[0]),len(image[0][0])), dtype=torch.float32, device="cpu")
+ masks = [zero_mask] * image_count
+ else:
+ mask = zero_mask
+ images.append(image)
+ masks.append(mask)
+ image_count += 1
+
+ if len(images) == 0:
+ raise FileNotFoundError(f"No images could be loaded from directory '{directory}'.")
+
+ return (torch.cat(images, dim=0), torch.stack(masks, dim=0), image_count)
+
+
+class LoadImagesFromDirectoryUpload:
+ @classmethod
+ def INPUT_TYPES(s):
+ input_dir = folder_paths.get_input_directory()
+ directories = []
+ for item in os.listdir(input_dir):
+ if not os.path.isfile(os.path.join(input_dir, item)) and item != "clipspace":
+ directories.append(item)
+ return {
+ "required": {
+ "directory": (directories,),
+ },
+ "optional": {
+ "image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
+ "skip_first_images": ("INT", {"default": 0, "min": 0, "step": 1}),
+ "select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE", "MASK", "INT")
+ FUNCTION = "load_images"
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
+
+ def load_images(self, directory: str, **kwargs):
+ directory = folder_paths.get_annotated_filepath(directory.strip())
+ return load_images(directory, **kwargs)
+
+ @classmethod
+ def IS_CHANGED(s, directory: str, **kwargs):
+ directory = folder_paths.get_annotated_filepath(directory.strip())
+ return is_changed_load_images(directory, **kwargs)
+
+ @classmethod
+ def VALIDATE_INPUTS(s, directory: str, **kwargs):
+ directory = folder_paths.get_annotated_filepath(directory.strip())
+ return validate_load_images(directory)
+
+
+class LoadImagesFromDirectoryPath:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "directory": ("STRING", {"default": "X://path/to/images", "vhs_path_extensions": []}),
+ },
+ "optional": {
+ "image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
+ "skip_first_images": ("INT", {"default": 0, "min": 0, "step": 1}),
+ "select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE", "MASK", "INT")
+ FUNCTION = "load_images"
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
+
+ def load_images(self, directory: str, **kwargs):
+ if directory is None or validate_load_images(directory) != True:
+ raise Exception("directory is not valid: " + directory)
+
+ return load_images(directory, **kwargs)
+
+ @classmethod
+ def IS_CHANGED(s, directory: str, **kwargs):
+ if directory is None:
+ return "input"
+ return is_changed_load_images(directory, **kwargs)
+
+ @classmethod
+ def VALIDATE_INPUTS(s, directory: str, **kwargs):
+ if directory is None:
+ return True
+ return validate_load_images(directory)
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/load_video_nodes.py b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/load_video_nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..c20257daa927706bb8dd0a6e0cbf6c69085a4d3f
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/load_video_nodes.py
@@ -0,0 +1,182 @@
+import os
+import numpy as np
+import torch
+from PIL import Image, ImageOps
+import cv2
+
+import folder_paths
+from comfy.utils import common_upscale
+from .logger import logger
+from .utils import calculate_file_hash, get_sorted_dir_files_from_directory, get_audio, lazy_eval, hash_path, validate_path
+
+
+video_extensions = ['webm', 'mp4', 'mkv', 'gif']
+
+
+def is_gif(filename) -> bool:
+ file_parts = filename.split('.')
+ return len(file_parts) > 1 and file_parts[-1] == "gif"
+
+
+def target_size(width, height, force_size) -> tuple[int, int]:
+ if force_size != "Disabled":
+ force_size = force_size.split("x")
+ if force_size[0] == "?":
+ width = (width*int(force_size[1]))//height
+ #Limit to a multple of 8 for latent conversion
+ #TODO: Consider instead cropping and centering to main aspect ratio
+ width = int(width)+4 & ~7
+ height = int(force_size[1])
+ elif force_size[1] == "?":
+ height = (height*int(force_size[0]))//width
+ height = int(height)+4 & ~7
+ width = int(force_size[0])
+ else:
+ width = int(force_size[0])
+ height = int(force_size[1])
+ return (width, height)
+
+def load_video_cv(video: str, force_rate: int, force_size: str, frame_load_cap: int, skip_first_frames: int, select_every_nth: int):
+ try:
+ video_cap = cv2.VideoCapture(video)
+ if not video_cap.isOpened():
+ raise ValueError(f"{video} could not be loaded with cv.")
+ # set video_cap to look at start_index frame
+ images = []
+ total_frame_count = 0
+ total_frames_evaluated = -1
+ frames_added = 0
+ base_frame_time = 1/video_cap.get(cv2.CAP_PROP_FPS)
+ width = video_cap.get(cv2.CAP_PROP_FRAME_WIDTH)
+ height = video_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
+ if force_rate == 0:
+ target_frame_time = base_frame_time
+ else:
+ target_frame_time = 1/force_rate
+ time_offset=target_frame_time - base_frame_time
+ while video_cap.isOpened():
+ if time_offset < target_frame_time:
+ is_returned, frame = video_cap.read()
+ # if didn't return frame, video has ended
+ if not is_returned:
+ break
+ time_offset += base_frame_time
+ if time_offset < target_frame_time:
+ continue
+ time_offset -= target_frame_time
+ # if not at start_index, skip doing anything with frame
+ total_frame_count += 1
+ if total_frame_count <= skip_first_frames:
+ continue
+ else:
+ total_frames_evaluated += 1
+
+ # if should not be selected, skip doing anything with frame
+ if total_frames_evaluated%select_every_nth != 0:
+ continue
+
+ # opencv loads images in BGR format (yuck), so need to convert to RGB for ComfyUI use
+ # follow up: can videos ever have an alpha channel?
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
+ # convert frame to comfyui's expected format (taken from comfy's load image code)
+ image = Image.fromarray(frame)
+ image = ImageOps.exif_transpose(image)
+ image = np.array(image, dtype=np.float32) / 255.0
+ image = torch.from_numpy(image)[None,]
+ images.append(image)
+ frames_added += 1
+ # if cap exists and we've reached it, stop processing frames
+ if frame_load_cap > 0 and frames_added >= frame_load_cap:
+ break
+ finally:
+ video_cap.release()
+ if len(images) == 0:
+ raise RuntimeError("No frames generated")
+ images = torch.cat(images, dim=0)
+ if force_size != "Disabled":
+ new_size = target_size(width, height, force_size)
+ if new_size[0] != width or new_size[1] != height:
+ s = images.movedim(-1,1)
+ s = common_upscale(s, new_size[0], new_size[1], "lanczos", "center")
+ images = s.movedim(1,-1)
+ # TODO: raise an error maybe if no frames were loaded?
+
+ #Setup lambda for lazy audio capture
+ audio = lambda : get_audio(video, skip_first_frames * target_frame_time,
+ frame_load_cap*target_frame_time)
+ return (images, frames_added, lazy_eval(audio))
+
+
+class LoadVideoUpload:
+ @classmethod
+ def INPUT_TYPES(s):
+ input_dir = folder_paths.get_input_directory()
+ files = []
+ for f in os.listdir(input_dir):
+ if os.path.isfile(os.path.join(input_dir, f)):
+ file_parts = f.split('.')
+ if len(file_parts) > 1 and (file_parts[-1] in video_extensions):
+ files.append(f)
+ return {"required": {
+ "video": (sorted(files),),
+ "force_rate": ("INT", {"default": 0, "min": 0, "max": 24, "step": 1}),
+ "force_size": (["Disabled", "256x?", "?x256", "256x256", "512x?", "?x512", "512x512"],),
+ "frame_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
+ "skip_first_frames": ("INT", {"default": 0, "min": 0, "step": 1}),
+ "select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
+ },}
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
+
+ RETURN_TYPES = ("IMAGE", "INT", "VHS_AUDIO", )
+ RETURN_NAMES = ("IMAGE", "frame_count", "audio",)
+ FUNCTION = "load_video"
+
+ def load_video(self, **kwargs):
+ kwargs['video'] = folder_paths.get_annotated_filepath(kwargs['video'].strip("\""))
+ return load_video_cv(**kwargs)
+
+ @classmethod
+ def IS_CHANGED(s, video, **kwargs):
+ image_path = folder_paths.get_annotated_filepath(video)
+ return calculate_file_hash(image_path)
+
+ @classmethod
+ def VALIDATE_INPUTS(s, video, force_size, **kwargs):
+ if not folder_paths.exists_annotated_filepath(video):
+ return "Invalid video file: {}".format(video)
+ return True
+
+
+class LoadVideoPath:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "video": ("STRING", {"default": "X://insert/path/here.mp4", "vhs_path_extensions": video_extensions}),
+ "force_rate": ("INT", {"default": 0, "min": 0, "max": 24, "step": 1}),
+ "force_size": (["Disabled", "256x?", "?x256", "256x256", "512x?", "?x512", "512x512"],),
+ "frame_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
+ "skip_first_frames": ("INT", {"default": 0, "min": 0, "step": 1}),
+ "select_every_nth": ("INT", {"default": 1, "min": 1, "step": 1}),
+ },
+ }
+
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
+
+ RETURN_TYPES = ("IMAGE", "INT", "VHS_AUDIO", )
+ RETURN_NAMES = ("IMAGE", "frame_count", "audio",)
+ FUNCTION = "load_video"
+
+ def load_video(self, **kwargs):
+ if kwargs['video'] is None or validate_path(kwargs['video']) != True:
+ raise Exception("video is not a valid path: " + kwargs['video'])
+ return load_video_cv(**kwargs)
+
+ @classmethod
+ def IS_CHANGED(s, video, **kwargs):
+ return hash_path(video)
+
+ @classmethod
+ def VALIDATE_INPUTS(s, video, force_size, **kwargs):
+ return validate_path(video, allow_none=True)
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/logger.py b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/logger.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e7b8d64bda275608ba6bf8ee28d2a2112e3e2be
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/logger.py
@@ -0,0 +1,36 @@
+import sys
+import copy
+import logging
+
+
+class ColoredFormatter(logging.Formatter):
+ COLORS = {
+ "DEBUG": "\033[0;36m", # CYAN
+ "INFO": "\033[0;32m", # GREEN
+ "WARNING": "\033[0;33m", # YELLOW
+ "ERROR": "\033[0;31m", # RED
+ "CRITICAL": "\033[0;37;41m", # WHITE ON RED
+ "RESET": "\033[0m", # RESET COLOR
+ }
+
+ def format(self, record):
+ colored_record = copy.copy(record)
+ levelname = colored_record.levelname
+ seq = self.COLORS.get(levelname, self.COLORS["RESET"])
+ colored_record.levelname = f"{seq}{levelname}{self.COLORS['RESET']}"
+ return super().format(colored_record)
+
+
+# Create a new logger
+logger = logging.getLogger("VideoHelperSuite")
+logger.propagate = False
+
+# Add handler if we don't have one.
+if not logger.handlers:
+ handler = logging.StreamHandler(sys.stdout)
+ handler.setFormatter(ColoredFormatter("[%(name)s] - %(levelname)s - %(message)s"))
+ logger.addHandler(handler)
+
+# Configure logger
+loglevel = logging.INFO
+logger.setLevel(loglevel)
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/nodes.py b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..55a995917888d050e0a365df4a9b0a06abccec49
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/nodes.py
@@ -0,0 +1,382 @@
+import os
+import sys
+import json
+import subprocess
+import numpy as np
+import re
+from typing import List
+from PIL import Image
+from PIL.PngImagePlugin import PngInfo
+from pathlib import Path
+
+import folder_paths
+from .logger import logger
+from .image_latent_nodes import DuplicateImages, DuplicateLatents, GetImageCount, GetLatentCount, MergeImages, MergeLatents, SelectEveryNthImage, SelectEveryNthLatent, SplitLatents, SplitImages
+from .load_video_nodes import LoadVideoUpload, LoadVideoPath
+from .load_images_nodes import LoadImagesFromDirectoryUpload, LoadImagesFromDirectoryPath
+from .utils import ffmpeg_path, get_audio, hash_path, validate_path
+
+folder_paths.folder_names_and_paths["VHS_video_formats"] = (
+ [
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "video_formats"),
+ ],
+ [".json"]
+)
+
+def gen_format_widgets(video_format):
+ for k in video_format:
+ if k.endswith("_pass"):
+ for i in range(len(video_format[k])):
+ if isinstance(video_format[k][i], list):
+ item = [video_format[k][i]]
+ yield item
+ video_format[k][i] = item[0]
+ else:
+ if isinstance(video_format[k], list):
+ item = [video_format[k]]
+ yield item
+ video_format[k] = item[0]
+
+def get_video_formats():
+ formats = []
+ for format_name in folder_paths.get_filename_list("VHS_video_formats"):
+ format_name = format_name[:-5]
+ video_format_path = folder_paths.get_full_path("VHS_video_formats", format_name + ".json")
+ with open(video_format_path, 'r') as stream:
+ video_format = json.load(stream)
+ widgets = [w[0] for w in gen_format_widgets(video_format)]
+ if (len(widgets) > 0):
+ formats.append(["video/" + format_name, widgets])
+ else:
+ formats.append("video/" + format_name)
+ return formats
+
+def apply_format_widgets(format_name, kwargs):
+ video_format_path = folder_paths.get_full_path("VHS_video_formats", format_name + ".json")
+ with open(video_format_path, 'r') as stream:
+ video_format = json.load(stream)
+ for w in gen_format_widgets(video_format):
+ assert(w[0][0] in kwargs)
+ w[0] = str(kwargs[w[0][0]])
+ return video_format
+
+def tensor_to_int(tensor, bits):
+ #TODO: investigate benefit of rounding by adding 0.5 before clip/cast
+ tensor = tensor.cpu().numpy() * (2**bits-1)
+ return np.clip(tensor, 0, (2**bits-1))
+def tensor_to_shorts(tensor):
+ return tensor_to_int(tensor, 16).astype(np.uint16)
+def tensor_to_bytes(tensor):
+ return tensor_to_int(tensor, 8).astype(np.uint8)
+
+class VideoCombine:
+ @classmethod
+ def INPUT_TYPES(s):
+ #Hide ffmpeg formats if ffmpeg isn't available
+ if ffmpeg_path is not None:
+ ffmpeg_formats = get_video_formats()
+ else:
+ ffmpeg_formats = []
+ return {
+ "required": {
+ "images": ("IMAGE",),
+ "frame_rate": (
+ "INT",
+ {"default": 8, "min": 1, "step": 1},
+ ),
+ "loop_count": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
+ "filename_prefix": ("STRING", {"default": "AnimateDiff"}),
+ "format": (["image/gif", "image/webp"] + ffmpeg_formats,),
+ "pingpong": ("BOOLEAN", {"default": False}),
+ "save_output": ("BOOLEAN", {"default": True}),
+ },
+ "optional": {
+ "audio": ("VHS_AUDIO",),
+ },
+ "hidden": {
+ "prompt": "PROMPT",
+ "extra_pnginfo": "EXTRA_PNGINFO",
+ "unique_id": "UNIQUE_ID"
+ },
+ }
+
+ RETURN_TYPES = ("VHS_FILENAMES",)
+ RETURN_NAMES = ("Filenames",)
+ OUTPUT_NODE = True
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
+ FUNCTION = "combine_video"
+
+ def combine_video(
+ self,
+ images,
+ frame_rate: int,
+ loop_count: int,
+ filename_prefix="AnimateDiff",
+ format="image/gif",
+ pingpong=False,
+ save_output=True,
+ prompt=None,
+ extra_pnginfo=None,
+ audio=None,
+ unique_id=None,
+ ):
+ kwargs = prompt[unique_id]['inputs']
+ # convert images to numpy
+
+ # get output information
+ output_dir = (
+ folder_paths.get_output_directory()
+ if save_output
+ else folder_paths.get_temp_directory()
+ )
+ (
+ full_output_folder,
+ filename,
+ _,
+ subfolder,
+ _,
+ ) = folder_paths.get_save_image_path(filename_prefix, output_dir)
+ output_files = []
+
+ metadata = PngInfo()
+ video_metadata = {}
+ if prompt is not None:
+ metadata.add_text("prompt", json.dumps(prompt))
+ video_metadata["prompt"] = prompt
+ if extra_pnginfo is not None:
+ for x in extra_pnginfo:
+ metadata.add_text(x, json.dumps(extra_pnginfo[x]))
+ video_metadata[x] = extra_pnginfo[x]
+
+ # comfy counter workaround
+ max_counter = 0
+
+ # Loop through the existing files
+ matcher = re.compile(f"{re.escape(filename)}_(\d+)\D*\.[a-zA-Z0-9]+")
+ for existing_file in os.listdir(full_output_folder):
+ # Check if the file matches the expected format
+ match = matcher.fullmatch(existing_file)
+ if match:
+ # Extract the numeric portion of the filename
+ file_counter = int(match.group(1))
+ # Update the maximum counter value if necessary
+ if file_counter > max_counter:
+ max_counter = file_counter
+
+ # Increment the counter by 1 to get the next available value
+ counter = max_counter + 1
+
+ # save first frame as png to keep metadata
+ file = f"{filename}_{counter:05}.png"
+ file_path = os.path.join(full_output_folder, file)
+ Image.fromarray(tensor_to_bytes(images[0])).save(
+ file_path,
+ pnginfo=metadata,
+ compress_level=4,
+ )
+ output_files.append(file_path)
+
+ format_type, format_ext = format.split("/")
+ if format_type == "image":
+ file = f"{filename}_{counter:05}.{format_ext}"
+ file_path = os.path.join(full_output_folder, file)
+ images = tensor_to_bytes(images)
+ if pingpong:
+ images = np.concatenate((images, images[-2:0:-1]))
+ frames = [Image.fromarray(f) for f in images]
+ # Use pillow directly to save an animated image
+ frames[0].save(
+ file_path,
+ format=format_ext.upper(),
+ save_all=True,
+ append_images=frames[1:],
+ duration=round(1000 / frame_rate),
+ loop=loop_count,
+ compress_level=4,
+ )
+ output_files.append(file_path)
+ else:
+ # Use ffmpeg to save a video
+ if ffmpeg_path is None:
+ #Should never be reachable
+ raise ProcessLookupError("Could not find ffmpeg")
+
+ video_format_path = folder_paths.get_full_path("VHS_video_formats", format_ext + ".json")
+ with open(video_format_path, 'r') as stream:
+ video_format = json.load(stream)
+ video_format = apply_format_widgets(format_ext, kwargs)
+ if video_format.get('input_color_depth', '8bit') == '16bit':
+ images = tensor_to_shorts(images)
+ i_pix_fmt = 'rgb48'
+ else:
+ images = tensor_to_bytes(images)
+ i_pix_fmt = 'rgb24'
+ if pingpong:
+ images = np.concatenate((images, images[-2:0:-1]))
+ file = f"{filename}_{counter:05}.{video_format['extension']}"
+ file_path = os.path.join(full_output_folder, file)
+ dimensions = f"{len(images[0][0])}x{len(images[0])}"
+ loop_args = ["-vf", "loop=loop=" + str(loop_count)+":size=" + str(len(images))]
+ args = [ffmpeg_path, "-v", "error", "-f", "rawvideo", "-pix_fmt", i_pix_fmt,
+ "-s", dimensions, "-r", str(frame_rate), "-i", "-"] \
+ + loop_args + video_format['main_pass']
+
+ env=os.environ.copy()
+ if "environment" in video_format:
+ env.update(video_format["environment"])
+ res = None
+ if video_format.get('save_metadata', 'False') != 'False':
+ os.makedirs(folder_paths.get_temp_directory(), exist_ok=True)
+ metadata = json.dumps(video_metadata)
+ metadata_path = os.path.join(folder_paths.get_temp_directory(), "metadata.txt")
+ #metadata from file should escape = ; # \ and newline
+ metadata = metadata.replace("\\","\\\\")
+ metadata = metadata.replace(";","\\;")
+ metadata = metadata.replace("#","\\#")
+ metadata = metadata.replace("=","\\=")
+ metadata = metadata.replace("\n","\\\n")
+ metadata = "comment=" + metadata
+ with open(metadata_path, "w") as f:
+ f.write(";FFMETADATA1\n")
+ f.write(metadata)
+ m_args = args[:1] + ["-i", metadata_path] + args[1:]
+ try:
+ res = subprocess.run(m_args + [file_path], input=images.tobytes(),
+ capture_output=True, check=True, env=env)
+ except subprocess.CalledProcessError as e:
+ #Check if output file exists. If it does, the re-execution
+ #will also fail. This obscures the cause of the error
+ #and seems to never occur concurrent to the metadata issue
+ if os.path.exists(file_path):
+ raise Exception("An error occured in the ffmpeg subprocess:\n" \
+ + e.stderr.decode("utf-8"))
+ #Res was not set
+ print(e.stderr.decode("utf-8"), end="", file=sys.stderr)
+ logger.warn("An error occurred when saving with metadata")
+
+ if not res:
+ try:
+ res = subprocess.run(args + [file_path], input=images.tobytes(),
+ capture_output=True, check=True, env=env)
+ except subprocess.CalledProcessError as e:
+ raise Exception("An error occured in the ffmpeg subprocess:\n" \
+ + e.stderr.decode("utf-8"))
+ if res.stderr:
+ print(res.stderr.decode("utf-8"), end="", file=sys.stderr)
+ output_files.append(file_path)
+
+
+ # Audio Injection after video is created, saves additional video with -audio.mp4
+
+ # Create audio file if input was provided
+ if audio:
+ output_file_with_audio = f"{filename}_{counter:05}-audio.{video_format['extension']}"
+ output_file_with_audio_path = os.path.join(full_output_folder, output_file_with_audio)
+ if "audio_pass" not in video_format:
+ logger.warn("Selected video format does not have explicit audio support")
+ video_format["audio_pass"] = ["-c:a", "libopus"]
+
+
+ # FFmpeg command with audio re-encoding
+ #TODO: expose audio quality options if format widgets makes it in
+ #Reconsider forcing apad/shortest
+ mux_args = [ffmpeg_path, "-v", "error", "-n", "-i", file_path,
+ "-i", "-", "-c:v", "copy"] \
+ + video_format["audio_pass"] \
+ + ["-af", "apad", "-shortest", output_file_with_audio_path]
+
+ try:
+ res = subprocess.run(mux_args, input=audio(), env=env,
+ capture_output=True, check=True)
+ except subprocess.CalledProcessError as e:
+ raise Exception("An error occured in the ffmpeg subprocess:\n" \
+ + e.stderr.decode("utf-8"))
+ if res.stderr:
+ print(res.stderr.decode("utf-8"), end="", file=sys.stderr)
+ output_files.append(output_file_with_audio_path)
+ #Return this file with audio to the webui.
+ #It will be muted unless opened or saved with right click
+ file = output_file_with_audio
+
+ previews = [
+ {
+ "filename": file,
+ "subfolder": subfolder,
+ "type": "output" if save_output else "temp",
+ "format": format,
+ }
+ ]
+ return {"ui": {"gifs": previews}, "result": ((save_output, output_files),)}
+ @classmethod
+ def VALIDATE_INPUTS(self, format, **kwargs):
+ return True
+
+class LoadAudio:
+ @classmethod
+ def INPUT_TYPES(s):
+ #Hide ffmpeg formats if ffmpeg isn't available
+ return {
+ "required": {
+ "audio_file": ("STRING", {"default": "input/", "vhs_path_extensions": ['wav','mp3','ogg','m4a','flac']}),
+ },
+ "optional" : {"seek_seconds": ("FLOAT", {"default": 0, "min": 0})}
+ }
+
+ RETURN_TYPES = ("VHS_AUDIO",)
+ RETURN_NAMES = ("audio",)
+ CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
+ FUNCTION = "load_audio"
+ def load_audio(self, audio_file, seek_seconds):
+ if audio_file is None or validate_path(audio_file) != True:
+ raise Exception("audio_file is not a valid path: " + audio_file)
+ #Eagerly fetch the audio since the user must be using it if the
+ #node executes, unlike Load Video
+ audio = get_audio(audio_file, start_time=seek_seconds)
+ return (lambda : audio,)
+
+ @classmethod
+ def IS_CHANGED(s, audio_file, seek_seconds):
+ return hash_path(audio_file)
+
+ @classmethod
+ def VALIDATE_INPUTS(s, audio_file, **kwargs):
+ return validate_path(audio_file, allow_none=True)
+
+NODE_CLASS_MAPPINGS = {
+ "VHS_VideoCombine": VideoCombine,
+ "VHS_LoadVideo": LoadVideoUpload,
+ "VHS_LoadVideoPath": LoadVideoPath,
+ "VHS_LoadImages": LoadImagesFromDirectoryUpload,
+ "VHS_LoadImagesPath": LoadImagesFromDirectoryPath,
+ "VHS_LoadAudio": LoadAudio,
+ # Latent and Image nodes
+ "VHS_SplitLatents": SplitLatents,
+ "VHS_SplitImages": SplitImages,
+ "VHS_MergeLatents": MergeLatents,
+ "VHS_MergeImages": MergeImages,
+ "VHS_SelectEveryNthLatent": SelectEveryNthLatent,
+ "VHS_SelectEveryNthImage": SelectEveryNthImage,
+ "VHS_GetLatentCount": GetLatentCount,
+ "VHS_GetImageCount": GetImageCount,
+ "VHS_DuplicateLatents": DuplicateLatents,
+ "VHS_DuplicateImages": DuplicateImages,
+}
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "VHS_VideoCombine": "Video Combine 🎥🅥🅗🅢",
+ "VHS_LoadVideo": "Load Video (Upload) 🎥🅥🅗🅢",
+ "VHS_LoadVideoPath": "Load Video (Path) 🎥🅥🅗🅢",
+ "VHS_LoadImages": "Load Images (Upload) 🎥🅥🅗🅢",
+ "VHS_LoadImagesPath": "Load Images (Path) 🎥🅥🅗🅢",
+ "VHS_LoadAudio": "Load Audio (Path)🎥🅥🅗🅢",
+ # Latent and Image nodes
+ "VHS_SplitLatents": "Split Latent Batch 🎥🅥🅗🅢",
+ "VHS_SplitImages": "Split Image Batch 🎥🅥🅗🅢",
+ "VHS_MergeLatents": "Merge Latent Batches 🎥🅥🅗🅢",
+ "VHS_MergeImages": "Merge Image Batches 🎥🅥🅗🅢",
+ "VHS_SelectEveryNthLatent": "Select Every Nth Latent 🎥🅥🅗🅢",
+ "VHS_SelectEveryNthImage": "Select Every Nth Image 🎥🅥🅗🅢",
+ "VHS_GetLatentCount": "Get Latent Count 🎥🅥🅗🅢",
+ "VHS_GetImageCount": "Get Image Count 🎥🅥🅗🅢",
+ "VHS_DuplicateLatents": "Duplicate Latent Batch 🎥🅥🅗🅢",
+ "VHS_DuplicateImages": "Duplicate Image Batch 🎥🅥🅗🅢",
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/server.py b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/server.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e0ed35fb6749f756481cf876b8721f4c099ccf5
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/server.py
@@ -0,0 +1,157 @@
+import server
+import folder_paths
+import os
+import time
+import subprocess
+from .utils import is_url, get_sorted_dir_files_from_directory, ffmpeg_path
+from comfy.k_diffusion.utils import FolderOfImages
+
+web = server.web
+
+def is_safe(path):
+ if "VHS_STRICT_PATHS" not in os.environ:
+ return True
+ basedir = os.path.abspath('.')
+ try:
+ common_path = os.path.commonpath([basedir, path])
+ except:
+ #Different drive on windows
+ return False
+ return common_path == basedir
+
+@server.PromptServer.instance.routes.get("/viewvideo")
+async def view_video(request):
+ query = request.rel_url.query
+ if "filename" not in query:
+ return web.Response(status=404)
+ filename = query["filename"]
+
+ #Path code misformats urls on windows and must be skipped
+ if is_url(filename):
+ file = filename
+ else:
+ filename, output_dir = folder_paths.annotated_filepath(filename)
+
+ type = request.rel_url.query.get("type", "output")
+ if type == "path":
+ #special case for path_based nodes
+ #NOTE: output_dir may be empty, but non-None
+ output_dir, filename = os.path.split(filename)
+ if output_dir is None:
+ output_dir = folder_paths.get_directory_by_type(type)
+
+ if output_dir is None:
+ return web.Response(status=400)
+
+ if not is_safe(output_dir):
+ return web.Response(status=403)
+
+ if "subfolder" in request.rel_url.query:
+ output_dir = os.path.join(output_dir, request.rel_url.query["subfolder"])
+
+ filename = os.path.basename(filename)
+ file = os.path.join(output_dir, filename)
+
+ if query.get('format', 'video') == 'folder':
+ if not os.path.isdir(file):
+ return web.Response(status=404)
+ else:
+ if not os.path.isfile(file):
+ return web.Response(status=404)
+
+ if query.get('format', 'video') == "folder":
+ #Check that folder contains some valid image file, get it's extension
+ #ffmpeg seems to not support list globs, so support for mixed extensions seems unfeasible
+ os.makedirs(folder_paths.get_temp_directory(), exist_ok=True)
+ concat_file = os.path.join(folder_paths.get_temp_directory(), "image_sequence_preview.txt")
+ skip_first_images = int(query.get('skip_first_images', 0))
+ select_every_nth = int(query.get('select_every_nth', 1))
+ valid_images = get_sorted_dir_files_from_directory(file, skip_first_images, select_every_nth, FolderOfImages.IMG_EXTENSIONS)
+ if len(valid_images) == 0:
+ return web.Response(status=400)
+ with open(concat_file, "w") as f:
+ f.write("ffconcat version 1.0\n")
+ for path in valid_images:
+ f.write("file '" + os.path.abspath(path) + "'\n")
+ f.write("duration 0.125\n")
+ in_args = ["-safe", "0", "-i", concat_file]
+ else:
+ in_args = ["-an", "-i", file]
+
+ args = [ffmpeg_path, "-v", "error"] + in_args
+ vfilters = []
+ if int(query.get('force_rate',0)) != 0:
+ vfilters.append("fps=fps="+query['force_rate'] + ":round=up:start_time=0.001")
+ if int(query.get('skip_first_frames', 0)) > 0:
+ vfilters.append(f"select=gt(n\\,{int(query['skip_first_frames'])-1})")
+ if int(query.get('select_every_nth', 1)) > 1:
+ vfilters.append(f"select=not(mod(n\\,{query['select_every_nth']}))")
+ if query.get('force_size','Disabled') != "Disabled":
+ size = query['force_size'].split('x')
+ if size[0] == '?' or size[1] == '?':
+ size[0] = "-2" if size[0] == '?' else f"'min({size[0]},iw)'"
+ size[1] = "-2" if size[1] == '?' else f"'min({size[1]},ih)'"
+ else:
+ #Aspect ratio is likely changed. A more complex command is required
+ #to crop the output to the new aspect ratio
+ ar = float(size[0])/float(size[1])
+ vfilters.append(f"crop=if(gt({ar}\\,a)\\,iw\\,ih*{ar}):if(gt({ar}\\,a)\\,iw/{ar}\\,ih)")
+ size = ':'.join(size)
+ vfilters.append(f"scale={size}")
+ vfilters.append("setpts=PTS-STARTPTS")
+ if len(vfilters) > 0:
+ args += ["-vf", ",".join(vfilters)]
+ if int(query.get('frame_load_cap', 0)) > 0:
+ args += ["-frames:v", query['frame_load_cap']]
+ #TODO:reconsider adding high frame cap/setting default frame cap on node
+
+ args += ['-c:v', 'libvpx-vp9','-deadline', 'realtime', '-cpu-used', '8', '-f', 'webm', '-']
+
+ try:
+ with subprocess.Popen(args, stdout=subprocess.PIPE) as proc:
+ try:
+ resp = web.StreamResponse()
+ resp.content_type = 'video/webm'
+ resp.headers["Content-Disposition"] = f"filename=\"{filename}\""
+ await resp.prepare(request)
+ while True:
+ bytes_read = proc.stdout.read()
+ if bytes_read is None:
+ #TODO: check for timeout here
+ time.sleep(.1)
+ continue
+ if len(bytes_read) == 0:
+ break
+ await resp.write(bytes_read)
+ except ConnectionResetError as e:
+ #Kill ffmpeg before stdout closes
+ proc.kill()
+ except BrokenPipeError as e:
+ pass
+ return resp
+
+@server.PromptServer.instance.routes.get("/getpath")
+async def get_path(request):
+ query = request.rel_url.query
+ if "path" not in query:
+ return web.Response(status=404)
+ path = os.path.abspath(query["path"])
+
+ if not os.path.exists(path) or not is_safe(path):
+ return web.json_response([])
+
+ #Use get so None is default instead of keyerror
+ valid_extensions = query.get("extensions")
+ valid_items = []
+ for item in os.scandir(path):
+ try:
+ if item.is_dir():
+ valid_items.append(item.name + "/")
+ continue
+ if valid_extensions is None or item.name.split(".")[-1] in valid_extensions:
+ valid_items.append(item.name)
+ except OSError:
+ #Broken symlinks can throw a very unhelpful "Invalid argument"
+ pass
+
+ return web.json_response(valid_items)
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/utils.py b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..44d96a2e7cd354a3c92e83bfadb0f7c8de9ed8e7
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/videohelpersuite/utils.py
@@ -0,0 +1,125 @@
+import hashlib
+import os
+from typing import Iterable
+import shutil
+import subprocess
+
+from .logger import logger
+
+def ffmpeg_suitability(path):
+ try:
+ version = subprocess.run([path, "-version"], check=True,
+ capture_output=True).stdout.decode("utf-8")
+ except:
+ return 0
+ score = 0
+ #rough layout of the importance of various features
+ simple_criterion = [("libvpx", 20),("264",10), ("265",3),
+ ("svtav1",5),("libopus", 1)]
+ for criterion in simple_criterion:
+ if version.find(criterion[0]) >= 0:
+ score += criterion[1]
+ #obtain rough compile year from copyright information
+ copyright_index = version.find('2000-2')
+ if copyright_index >= 0:
+ copyright_year = version[copyright_index+6:copyright_index+9]
+ if copyright_year.isnumeric():
+ score += int(copyright_year)
+ return score
+
+if "VHS_FORCE_FFMPEG_PATH" in os.environ:
+ ffmpeg_path = os.env["VHS_FORCE_FFMPEG_PATH"]
+else:
+ ffmpeg_paths = []
+ try:
+ from imageio_ffmpeg import get_ffmpeg_exe
+ imageio_ffmpeg_path = get_ffmpeg_exe()
+ ffmpeg_paths.append(imageio_ffmpeg_path)
+ except:
+ if "VHS_USE_IMAGEIO_FFMPEG" in os.environ:
+ raise
+ logger.warn("Failed to import imageio_ffmpeg")
+ if "VHS_USE_IMAGEIO_FFMPEG" in os.environ:
+ ffmpeg_path = imageio_ffmpeg_path
+ else:
+ system_ffmpeg = shutil.which("ffmpeg")
+ if system_ffmpeg is not None:
+ ffmpeg_paths.append(system_ffmpeg)
+ if len(ffmpeg_paths) == 0:
+ logger.error("No valid ffmpeg found.")
+ ffmpeg_path = None
+ else:
+ ffmpeg_path = max(ffmpeg_paths, key=ffmpeg_suitability)
+
+def get_sorted_dir_files_from_directory(directory: str, skip_first_images: int=0, select_every_nth: int=1, extensions: Iterable=None):
+ directory = directory.strip()
+ dir_files = os.listdir(directory)
+ dir_files = sorted(dir_files)
+ dir_files = [os.path.join(directory, x) for x in dir_files]
+ dir_files = list(filter(lambda filepath: os.path.isfile(filepath), dir_files))
+ # filter by extension, if needed
+ if extensions is not None:
+ extensions = list(extensions)
+ new_dir_files = []
+ for filepath in dir_files:
+ ext = "." + filepath.split(".")[-1]
+ if ext.lower() in extensions:
+ new_dir_files.append(filepath)
+ dir_files = new_dir_files
+ # start at skip_first_images
+ dir_files = dir_files[skip_first_images:]
+ dir_files = dir_files[0::select_every_nth]
+ return dir_files
+
+# modified from https://stackoverflow.com/questions/22058048/hashing-a-file-in-python
+def calculate_file_hash(filename: str, hash_every_n: int = 1):
+ h = hashlib.sha256()
+ b = bytearray(10*1024*1024) # read 10 megabytes at a time
+ mv = memoryview(b)
+ with open(filename, 'rb', buffering=0) as f:
+ i = 0
+ # don't hash entire file, only portions of it if requested
+ while n := f.readinto(mv):
+ if i%hash_every_n == 0:
+ h.update(mv[:n])
+ i += 1
+ return h.hexdigest()
+
+def get_audio(file, start_time=0, duration=0):
+ args = [ffmpeg_path, "-v", "error", "-i", file]
+ if start_time > 0:
+ args += ["-ss", str(start_time)]
+ if duration > 0:
+ args += ["-t", str(duration)]
+ return subprocess.run(args + ["-f", "wav", "-"],
+ stdout=subprocess.PIPE, check=True).stdout
+def lazy_eval(func):
+ class Cache:
+ def __init__(self, func):
+ self.res = None
+ self.func = func
+ def get(self):
+ if self.res is None:
+ self.res = self.func()
+ return self.res
+ cache = Cache(func)
+ return lambda : cache.get()
+
+def is_url(url):
+ return url.split("://")[0] in ["http", "https"]
+
+def hash_path(path):
+ if path is None:
+ return "input"
+ if is_url(path):
+ return "url"
+ return calculate_file_hash(path.strip("\""))
+def validate_path(path, allow_none=False, allow_url=True):
+ if path is None:
+ return allow_none
+ if is_url(path):
+ #Probably not feasible to check if url resolves here
+ return True if allow_url else "URLs are unsupported for this path"
+ if not os.path.isfile(path.strip("\"")):
+ return "Invalid file path: {}".format(path)
+ return True
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/web/js/VHS.core.js b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/web/js/VHS.core.js
new file mode 100644
index 0000000000000000000000000000000000000000..8776aa58af6f9c8973ccc86af648b80209215799
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/web/js/VHS.core.js
@@ -0,0 +1,1117 @@
+import { app } from '../../../scripts/app.js'
+import { api } from '../../../scripts/api.js'
+import { applyTextReplacements } from "../../../scripts/utils.js";
+
+
+function chainCallback(object, property, callback) {
+ if (object == undefined) {
+ //This should not happen.
+ console.error("Tried to add callback to non-existant object")
+ return;
+ }
+ if (property in object) {
+ const callback_orig = object[property]
+ object[property] = function () {
+ const r = callback_orig.apply(this, arguments);
+ callback.apply(this, arguments);
+ return r
+ };
+ } else {
+ object[property] = callback;
+ }
+}
+
+function injectHidden(widget) {
+ widget.computeSize = (target_width) => {
+ if (widget.hidden) {
+ return [0, -4];
+ }
+ return [target_width, 20];
+ };
+ widget._type = widget.type
+ Object.defineProperty(widget, "type", {
+ set : function(value) {
+ widget._type = value;
+ },
+ get : function() {
+ if (widget.hidden) {
+ return "hidden";
+ }
+ return widget._type;
+ }
+ });
+}
+
+const convDict = {
+ VHS_LoadImages : ["directory", null, "image_load_cap", "skip_first_images", "select_every_nth"],
+ VHS_LoadImagesPath : ["directory", "image_load_cap", "skip_first_images", "select_every_nth"],
+ VHS_VideoCombine : ["frame_rate", "loop_count", "filename_prefix", "format", "pingpong", "save_image"],
+ VHS_LoadVideo : ["video", "force_rate", "force_size", "frame_load_cap", "skip_first_frames", "select_every_nth"],
+ VHS_LoadVideoPath : ["video", "force_rate", "force_size", "frame_load_cap", "skip_first_frames", "select_every_nth"]
+};
+const renameDict = {VHS_VideoCombine : {save_output : "save_image"}}
+function useKVState(nodeType) {
+ chainCallback(nodeType.prototype, "onNodeCreated", function () {
+ chainCallback(this, "onConfigure", function(info) {
+ if (!this.widgets) {
+ //Node has no widgets, there is nothing to restore
+ return
+ }
+ if (typeof(info.widgets_values) != "object") {
+ //widgets_values is in some unknown inactionable format
+ return
+ }
+ let widgetDict = info.widgets_values
+ if (info.widgets_values.length) {
+ //widgets_values is in the old list format
+ if (this.type in convDict) {
+ //widget does not have a conversion format provided
+ let convList = convDict[this.type];
+ if(info.widgets_values.length >= convList.length) {
+ //has all required fields
+ widgetDict = {}
+ for (let i = 0; i < convList.length; i++) {
+ if(!convList[i]) {
+ //Element should not be processed (upload button on load image sequence)
+ continue
+ }
+ widgetDict[convList[i]] = info.widgets_values[i];
+ }
+ } else {
+ //widgets_values is missing elements marked as required
+ //let it fall through to failure state
+ }
+ }
+ }
+ if (widgetDict.length == undefined) {
+ for (let w of this.widgets) {
+ if (w.name in widgetDict) {
+ w.value = widgetDict[w.name];
+ } else {
+ //Check for a legacy name that needs migrating
+ if (this.type in renameDict && w.name in renameDict[this.type]) {
+ if (renameDict[this.type][w.name] in widgetDict) {
+ w.value = widgetDict[renameDict[this.type][w.name]]
+ continue
+ }
+ }
+ //attempt to restore default value
+ let inputs = LiteGraph.getNodeType(this.type).nodeData.input;
+ let initialValue = null;
+ if (inputs?.required?.hasOwnProperty(w.name)) {
+ if (inputs.required[w.name][1]?.hasOwnProperty("default")) {
+ initialValue = inputs.required[w.name][1].default;
+ } else if (inputs.required[w.name][0].length) {
+ initialValue = inputs.required[w.name][0][0];
+ }
+ } else if (inputs?.optional?.hasOwnProperty(w.name)) {
+ if (inputs.optional[w.name][1]?.hasOwnProperty("default")) {
+ initialValue = inputs.optional[w.name][1].default;
+ } else if (inputs.optional[w.name][0].length) {
+ initialValue = inputs.optional[w.name][0][0];
+ }
+ }
+ if (initialValue) {
+ w.value = initialValue;
+ }
+ }
+ }
+ } else {
+ //Saved data was not a map made by this method
+ //and a conversion dict for it does not exist
+ //It's likely an array and that has been blindly applied
+ if (info?.widgets_values?.length != this.widgets.length) {
+ //Widget could not have restored properly
+ //Note if multiple node loads fail, only the latest error dialog displays
+ app.ui.dialog.show("Failed to restore node: " + this.title + "\nPlease remove and re-add it.")
+ this.bgcolor = "#C00"
+ }
+ }
+ });
+ chainCallback(this, "onSerialize", function(info) {
+ info.widgets_values = {};
+ if (!this.widgets) {
+ //object has no widgets, there is nothing to store
+ return;
+ }
+ for (let w of this.widgets) {
+ info.widgets_values[w.name] = w.value;
+ }
+ });
+ })
+}
+
+function fitHeight(node) {
+ node.setSize([node.size[0], node.computeSize([node.size[0], node.size[1]])[1]])
+ node?.graph?.setDirtyCanvas(true);
+}
+
+async function uploadFile(file) {
+ //TODO: Add uploaded file to cache with Cache.put()?
+ try {
+ // Wrap file in formdata so it includes filename
+ const body = new FormData();
+ const i = file.webkitRelativePath.lastIndexOf('/');
+ const subfolder = file.webkitRelativePath.slice(0,i+1)
+ const new_file = new File([file], file.name, {
+ type: file.type,
+ lastModified: file.lastModified,
+ });
+ body.append("image", new_file);
+ if (i > 0) {
+ body.append("subfolder", subfolder);
+ }
+ const resp = await api.fetchApi("/upload/image", {
+ method: "POST",
+ body,
+ });
+
+ if (resp.status === 200) {
+ return resp.status
+ } else {
+ alert(resp.status + " - " + resp.statusText);
+ }
+ } catch (error) {
+ alert(error);
+ }
+}
+
+function addDateFormatting(nodeType, field, timestamp_widget = false) {
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ const widget = this.widgets.find((w) => w.name === field);
+ widget.serializeValue = () => {
+ return applyTextReplacements(app, widget.value);
+ };
+ });
+}
+function addTimestampWidget(nodeType, nodeData, targetWidget) {
+ const newWidgets = {};
+ for (let key in nodeData.input.required) {
+ if (key == targetWidget) {
+ //TODO: account for duplicate entries?
+ newWidgets["timestamp_directory"] = ["BOOLEAN", {"default": true}]
+ }
+ newWidgets[key] = nodeData.input.required[key];
+ }
+ nodeDta.input.required = newWidgets;
+ chainCallback(nodeType.prototype, "onNodeCreated", function () {
+ const directoryWidget = this.widgets.find((w) => w.name === "directory_name");
+ const timestampWidget = this.widgets.find((w) => w.name === "timestamp_directory");
+ directoryWidget.serializeValue = () => {
+ if (timestampWidget.value) {
+ //ignore actual value and return timestamp
+ return formatDate("yyyy-MM-ddThh:mm:ss", new Date());
+ }
+ return directoryWidget.value
+ };
+ timestampWidget._value = value;
+ Object.definteProperty(timestampWidget, "value", {
+ set : function(value) {
+ this._value = value;
+ directoryWidget.disabled = value;
+ },
+ get : function() {
+ return this._value;
+ }
+ });
+ });
+}
+
+function addCustomSize(nodeType, nodeData, widgetName) {
+ //Add the extra size widgets now
+ //This takes some finagling as widget order is defined by key order
+ const newWidgets = {};
+ for (let key in nodeData.input.required) {
+ newWidgets[key] = nodeData.input.required[key]
+ if (key == widgetName) {
+ newWidgets[key][0] = newWidgets[key][0].concat(["Custom Width", "Custom Height", "Custom"])
+ newWidgets["custom_width"] = ["INT", {"default": 512, "min": 8, "step": 8}]
+ newWidgets["custom_height"] = ["INT", {"default": 512, "min": 8, "step": 8}]
+ }
+ }
+ nodeData.input.required = newWidgets;
+
+ //Add a callback which sets up the actual logic once the node is created
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ const node = this;
+ const sizeOptionWidget = node.widgets.find((w) => w.name === widgetName);
+ const widthWidget = node.widgets.find((w) => w.name === "custom_width");
+ const heightWidget = node.widgets.find((w) => w.name === "custom_height");
+ injectHidden(widthWidget);
+ widthWidget.options.serialize = false;
+ injectHidden(heightWidget);
+ heightWidget.options.serialize = false;
+ sizeOptionWidget._value = sizeOptionWidget.value;
+ Object.defineProperty(sizeOptionWidget, "value", {
+ set : function(value) {
+ //TODO: Only modify hidden/reset size when a change occurs
+ if (value == "Custom Width") {
+ widthWidget.hidden = false;
+ heightWidget.hidden = true;
+ } else if (value == "Custom Height") {
+ widthWidget.hidden = true;
+ heightWidget.hidden = false;
+ } else if (value == "Custom") {
+ widthWidget.hidden = false;
+ heightWidget.hidden = false;
+ } else{
+ widthWidget.hidden = true;
+ heightWidget.hidden = true;
+ }
+ node.setSize([node.size[0], node.computeSize([node.size[0], node.size[1]])[1]])
+ this._value = value;
+ },
+ get : function() {
+ return this._value;
+ }
+ });
+ //prevent clobbering of new options on refresh
+ sizeOptionWidget.options._values = sizeOptionWidget.options.values;
+ Object.defineProperty(sizeOptionWidget.options, "values", {
+ set : function(values) {
+ this._values = values;
+ this._values.push("Custom Width", "Custom Height", "Custom");
+ },
+ get : function() {
+ return this._values;
+ }
+ });
+ //Ensure proper visibility/size state for initial value
+ sizeOptionWidget.value = sizeOptionWidget._value;
+
+ sizeOptionWidget.serializeValue = function() {
+ if (this.value == "Custom Width") {
+ return widthWidget.value + "x?";
+ } else if (this.value == "Custom Height") {
+ return "?x" + heightWidget.value;
+ } else if (this.value == "Custom") {
+ return widthWidget.value + "x" + heightWidget.value;
+ } else {
+ return this.value;
+ }
+ };
+ });
+}
+function addUploadWidget(nodeType, nodeData, widgetName, type="video") {
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ const pathWidget = this.widgets.find((w) => w.name === widgetName);
+ const fileInput = document.createElement("input");
+ chainCallback(this, "onRemoved", () => {
+ fileInput?.remove();
+ });
+ if (type == "folder") {
+ Object.assign(fileInput, {
+ type: "file",
+ style: "display: none",
+ webkitdirectory: true,
+ onchange: async () => {
+ const directory = fileInput.files[0].webkitRelativePath;
+ const i = directory.lastIndexOf('/');
+ if (i <= 0) {
+ throw "No directory found";
+ }
+ const path = directory.slice(0,directory.lastIndexOf('/'))
+ if (pathWidget.options.values.includes(path)) {
+ alert("A folder of the same name already exists");
+ return;
+ }
+ let successes = 0;
+ for(const file of fileInput.files) {
+ if (await uploadFile(file) == 200) {
+ successes++;
+ } else {
+ //Upload failed, but some prior uploads may have succeeded
+ //Stop future uploads to prevent cascading failures
+ //and only add to list if an upload has succeeded
+ if (successes > 0) {
+ break
+ } else {
+ return;
+ }
+ }
+ }
+ pathWidget.options.values.push(path);
+ pathWidget.value = path;
+ },
+ });
+ } else if (type == "video") {
+ Object.assign(fileInput, {
+ type: "file",
+ accept: "video/webm,video/mp4,video/mkv,image/gif",
+ style: "display: none",
+ onchange: async () => {
+ if (fileInput.files.length) {
+ if (await uploadFile(fileInput.files[0]) != 200) {
+ //upload failed and file can not be added to options
+ return;
+ }
+ const filename = fileInput.files[0].name;
+ pathWidget.options.values.push(filename);
+ pathWidget.value = filename;
+ }
+ },
+ });
+ } else {
+ throw "Unknown upload type"
+ }
+ document.body.append(fileInput);
+ let uploadWidget = this.addWidget("button", "choose " + type + " to upload", "image", () => {
+ //clear the active click event
+ app.canvas.node_widget = null
+
+ fileInput.click();
+ });
+ uploadWidget.options.serialize = false;
+ });
+}
+
+function addVideoPreview(nodeType) {
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ let previewNode = this;
+ //preview is a made up widget type to enable user defined functions
+ //videopreview is widget name
+ //The previous implementation used type to distinguish between a video and gif,
+ //but the type is not serialized and would not survive a reload
+ var previewWidget = { name : "videopreview", type : "preview",
+ draw : function(ctx, node, widgetWidth, widgetY, height) {
+ //update widget position, hide if off-screen
+ const transform = ctx.getTransform();
+ const scale = app.canvas.ds.scale;//gets the litegraph zoom
+ //calculate coordinates with account for browser zoom
+ const x = transform.e*scale/transform.a;
+ const y = transform.f*scale/transform.a;
+ Object.assign(this.parentEl.style, {
+ left: (x+15*scale) + "px",
+ top: (y + widgetY*scale) + "px",
+ width: ((widgetWidth-30)*scale) + "px",
+ zIndex: 2 + (node.is_selected ? 1 : 0),
+ position: "absolute",
+ });
+ this._boundingCount = 0;
+ },
+ computeSize : function(width) {
+ if (this.aspectRatio && !this.parentEl.hidden) {
+ let height = (previewNode.size[0]-30)/ this.aspectRatio;
+ if (!(height > 0)) {
+ height = 0;
+ }
+ return [width, height];
+ }
+ return [width, -4];//no loaded src, widget should not display
+ },
+ value : {hidden: false, paused: false, params: {}}
+ };
+ //onRemoved isn't a litegraph supported function on widgets
+ //Given that onremoved widget and node callbacks are sparse, this
+ //saves the required iteration.
+ chainCallback(this, "onRemoved", () => {
+ previewWidget?.parentEl?.remove();
+ });
+ previewWidget.options = {serialize : false};
+ this.addCustomWidget(previewWidget);
+ previewWidget.parentEl = document.createElement("div");
+ previewWidget.parentEl.className = "vhs_preview";
+ previewWidget.parentEl.style['pointer-events'] = "none"
+
+ previewWidget.videoEl = document.createElement("video");
+ previewWidget.videoEl.controls = false;
+ previewWidget.videoEl.loop = true;
+ previewWidget.videoEl.muted = true;
+ previewWidget.videoEl.style['width'] = "100%"
+ previewWidget.videoEl.addEventListener("loadedmetadata", () => {
+
+ previewWidget.aspectRatio = previewWidget.videoEl.videoWidth / previewWidget.videoEl.videoHeight;
+ fitHeight(this);
+ });
+ previewWidget.videoEl.addEventListener("error", () => {
+ //TODO: consider a way to properly notify the user why a preview isn't shown.
+ previewWidget.parentEl.hidden = true;
+ fitHeight(this);
+ });
+
+ previewWidget.imgEl = document.createElement("img");
+ previewWidget.imgEl.style['width'] = "100%"
+ previewWidget.imgEl.hidden = true;
+ previewWidget.imgEl.onload = () => {
+ previewWidget.aspectRatio = previewWidget.imgEl.naturalWidth / previewWidget.imgEl.naturalHeight;
+ fitHeight(this);
+ };
+
+ var timeout = null;
+ this.updateParameters = (params, force_update) => {
+ if (!previewWidget.value.params) {
+ if(typeof(previewWidget.value != 'object')) {
+ previewWidget.value = {hidden: false, paused: false}
+ }
+ previewWidget.value.params = {}
+ }
+ Object.assign(previewWidget.value.params, params)
+ if (!force_update &&
+ !app.ui.settings.getSettingValue("VHS.AdvancedPreviews", false)) {
+ return;
+ }
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ if (force_update) {
+ previewWidget.updateSource();
+ } else {
+ timeout = setTimeout(() => previewWidget.updateSource(),100);
+ }
+ };
+ previewWidget.updateSource = function () {
+ if (this.value.params == undefined) {
+ return;
+ }
+ let params = {}
+ Object.assign(params, this.value.params);//shallow copy
+ this.parentEl.hidden = this.value.hidden;
+ if (params.format?.split('/')[0] == 'video' ||
+ app.ui.settings.getSettingValue("VHS.AdvancedPreviews", false) &&
+ (params.format?.split('/')[1] == 'gif') || params.format == 'folder') {
+ this.videoEl.autoplay = !this.value.paused && !this.value.hidden;
+ let target_width = 256
+ if (this.parentEl.style?.width) {
+ //overscale to allow scrolling. Endpoint won't return higher than native
+ target_width = this.parentEl.style.width.slice(0,-2)*2;
+ }
+ if (!params.force_size || params.force_size.includes("?") || params.force_size == "Disabled") {
+ params.force_size = target_width+"x?"
+ } else {
+ let size = params.force_size.split("x")
+ let ar = parseInt(size[0])/parseInt(size[1])
+ params.force_size = target_width+"x"+(target_width/ar)
+ }
+ if (app.ui.settings.getSettingValue("VHS.AdvancedPreviews", false)) {
+ this.videoEl.src = api.apiURL('/viewvideo?' + new URLSearchParams(params));
+ } else {
+ previewWidget.videoEl.src = api.apiURL('/view?' + new URLSearchParams(params));
+ }
+ this.videoEl.hidden = false;
+ this.imgEl.hidden = true;
+ } else if (params.format?.split('/')[0] == 'image'){
+ //Is animated image
+ this.imgEl.src = api.apiURL('/view?' + new URLSearchParams(params));
+ this.videoEl.hidden = true;
+ this.imgEl.hidden = false;
+ }
+ }
+ //Hide video element if offscreen
+ //The multiline input implementation moves offscreen every frame
+ //and doesn't apply until a node with an actual inputEl is loaded
+ this._boundingCount = 0;
+ this.onBounding = function() {
+ if (this._boundingCount++>5) {
+ previewWidget.parentEl.style.left = "-8000px";
+ }
+ }
+ previewWidget.parentEl.appendChild(previewWidget.videoEl)
+ previewWidget.parentEl.appendChild(previewWidget.imgEl)
+ document.body.appendChild(previewWidget.parentEl);
+ });
+}
+function addPreviewOptions(nodeType) {
+ chainCallback(nodeType.prototype, "getExtraMenuOptions", function(_, options) {
+ // The intended way of appending options is returning a list of extra options,
+ // but this isn't used in widgetInputs.js and would require
+ // less generalization of chainCallback
+ let optNew = []
+ const previewWidget = this.widgets.find((w) => w.name === "videopreview");
+
+ let url = null
+ if (previewWidget.videoEl?.hidden == false && previewWidget.videoEl.src) {
+ //Use full quality video
+ url = api.apiURL('/view?' + new URLSearchParams(previewWidget.value.params));
+ } else if (previewWidget.imgEl?.hidden == false && previewWidget.imgEl.src) {
+ url = previewWidget.imgEl.src;
+ url = new URL(url);
+ }
+ if (url) {
+ optNew.push(
+ {
+ content: "Open preview",
+ callback: () => {
+ window.open(url, "_blank")
+ },
+ },
+ {
+ content: "Save preview",
+ callback: () => {
+ const a = document.createElement("a");
+ a.href = url;
+ a.setAttribute("download", new URLSearchParams(previewWidget.value.params).get("filename"));
+ document.body.append(a);
+ a.click();
+ requestAnimationFrame(() => a.remove());
+ },
+ }
+ );
+ }
+ const PauseDesc = (previewWidget.value.paused ? "Resume" : "Pause") + " preview";
+ if(previewWidget.videoEl.hidden == false) {
+ optNew.push({content: PauseDesc, callback: () => {
+ //animated images can't be paused and are more likely to cause performance issues.
+ //changing src to a single keyframe is possible,
+ //For now, the option is disabled if an animated image is being displayed
+ if(previewWidget.value.paused) {
+ previewWidget.videoEl?.play();
+ } else {
+ previewWidget.videoEl?.pause();
+ }
+ previewWidget.value.paused = !previewWidget.value.paused;
+ }});
+ }
+ //TODO: Consider hiding elements if no video preview is available yet.
+ //It would reduce confusion at the cost of functionality
+ //(if a video preview lags the computer, the user should be able to hide in advance)
+ const visDesc = (previewWidget.value.hidden ? "Show" : "Hide") + " preview";
+ optNew.push({content: visDesc, callback: () => {
+ if (!previewWidget.videoEl.hidden && !previewWidget.value.hidden) {
+ previewWidget.videoEl.pause();
+ } else if (previewWidget.value.hidden && !previewWidget.videoEl.hidden && !previewWidget.value.paused) {
+ previewWidget.videoEl.play();
+ }
+ previewWidget.value.hidden = !previewWidget.value.hidden;
+ previewWidget.parentEl.hidden = previewWidget.value.hidden;
+ fitHeight(this);
+
+ }});
+ optNew.push({content: "Sync preview", callback: () => {
+ //TODO: address case where videos have varying length
+ //Consider a system of sync groups which are opt-in?
+ for (let p of document.getElementsByClassName("vhs_preview")) {
+ for (let child of p.children) {
+ if (child.tagName == "VIDEO") {
+ child.currentTime=0;
+ } else if (child.tagName == "IMG") {
+ child.src = child.src;
+ }
+ }
+ }
+ }});
+ if(options.length > 0 && options[0] != null && optNew.length > 0) {
+ optNew.push(null);
+ }
+ options.unshift(...optNew);
+ });
+}
+function addFormatWidgets(nodeType) {
+ function parseFormats(options) {
+ options.fullvalues = options._values;
+ options._values = [];
+ for (let format of options.fullvalues) {
+ if (Array.isArray(format)) {
+ options._values.push(format[0]);
+ } else {
+ options._values.push(format);
+ }
+ }
+ }
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ var formatWidget = null;
+ var formatWidgetIndex = -1;
+ for(let i = 0; i < this.widgets.length; i++) {
+ if (this.widgets[i].name === "format"){
+ formatWidget = this.widgets[i];
+ formatWidgetIndex = i+1;
+ }
+ }
+ let formatWidgetsCount = 0;
+ //Pre-process options to just names
+ formatWidget.options._values = formatWidget.options.values;
+ parseFormats(formatWidget.options);
+ Object.defineProperty(formatWidget.options, "values", {
+ set : (value) => {
+ formatWidget.options._values = value;
+ parseFormats(formatWidget.options);
+ },
+ get : () => {
+ return formatWidget.options._values;
+ }
+ })
+
+ formatWidget._value = formatWidget.value;
+ Object.defineProperty(formatWidget, "value", {
+ set : (value) => {
+ formatWidget._value = value;
+ let newWidgets = [];
+ const fullDef = formatWidget.options.fullvalues.find((w) => Array.isArray(w) ? w[0] === value : w === value);
+ if (!Array.isArray(fullDef)) {
+ formatWidget._value = value;
+ } else {
+ formatWidget._value = fullDef[0];
+ for (let wDef of fullDef[1]) {
+ //create widgets. Heavy borrowed from web/scripts/app.js
+ //default implementation doesn't work since it automatically adds
+ //the widget in the wrong spot.
+ //TODO: consider letting this happen and just removing from list?
+ let w = {};
+ w.name = wDef[0];
+ let inputData = wDef.slice(1);
+ w.type = inputData[0];
+ w.options = inputData[1] ? inputData[1] : {};
+ if (Array.isArray(w.type)) {
+ w.value = w.type[0];
+ w.options.values = w.type;
+ w.type = "combo";
+ }
+ if(inputData[1]?.default) {
+ w.value = inputData[1].default;
+ }
+ if (w.type == "INT") {
+ Object.assign(w.options, {"precision": 0, "step": 10})
+ w.callback = function (v) {
+ const s = this.options.step / 10;
+ this.value = Math.round(v / s) * s;
+ }
+ }
+ const typeTable = {BOOLEAN: "toggle", STRING: "text", INT: "number", FLOAT: "number"};
+ if (w.type in typeTable) {
+ w.type = typeTable[w.type];
+ }
+ newWidgets.push(w);
+ }
+ }
+ this.widgets.splice(formatWidgetIndex, formatWidgetsCount, ...newWidgets);
+ fitHeight(this);
+ formatWidgetsCount = newWidgets.length;
+ },
+ get : () => {
+ return formatWidget._value;
+ }
+ });
+ });
+}
+function addLoadVideoCommon(nodeType, nodeData) {
+ addCustomSize(nodeType, nodeData, "force_size")
+ addVideoPreview(nodeType);
+ addPreviewOptions(nodeType);
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ const pathWidget = this.widgets.find((w) => w.name === "video");
+ const frameCapWidget = this.widgets.find((w) => w.name === 'frame_load_cap');
+ const frameSkipWidget = this.widgets.find((w) => w.name === 'skip_first_frames');
+ const rateWidget = this.widgets.find((w) => w.name === 'force_rate');
+ const skipWidget = this.widgets.find((w) => w.name === 'select_every_nth');
+ const sizeWidget = this.widgets.find((w) => w.name === 'force_size');
+ //widget.callback adds unused arguements which need culling
+ let update = function (value, _, node) {
+ let param = {}
+ param[this.name] = value
+ node.updateParameters(param);
+ }
+ chainCallback(frameCapWidget, "callback", update);
+ chainCallback(frameSkipWidget, "callback", update);
+ chainCallback(rateWidget, "callback", update);
+ chainCallback(skipWidget, "callback", update);
+ let updateSize = function(value, _, node) {
+ node.updateParameters({"force_size": sizeWidget.serializeValue()})
+ }
+ chainCallback(sizeWidget, "callback", updateSize);
+ chainCallback(this.widgets.find((w) => w.name === "custom_width"), "callback", updateSize);
+ chainCallback(this.widgets.find((w) => w.name === "custom_height"), "callback", updateSize);
+
+ //do first load
+ requestAnimationFrame(() => {
+ for (let w of [frameCapWidget, frameSkipWidget, rateWidget, pathWidget, skipWidget]) {
+ w.callback(w.value, null, this);
+ }
+ });
+ });
+}
+function addLoadImagesCommon(nodeType, nodeData) {
+ addVideoPreview(nodeType);
+ addPreviewOptions(nodeType);
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ const pathWidget = this.widgets.find((w) => w.name === "directory");
+ const frameCapWidget = this.widgets.find((w) => w.name === 'image_load_cap');
+ const frameSkipWidget = this.widgets.find((w) => w.name === 'skip_first_images');
+ const skipWidget = this.widgets.find((w) => w.name === 'select_every_nth');
+ //widget.callback adds unused arguements which need culling
+ let update = function (value, _, node) {
+ let param = {}
+ param[this.name] = value
+ node.updateParameters(param);
+ }
+ chainCallback(frameCapWidget, "callback", (value, _, node) => {
+ node.updateParameters({frame_load_cap: value})
+ });
+ chainCallback(frameSkipWidget, "callback", update);
+ chainCallback(skipWidget, "callback", update);
+ //do first load
+ requestAnimationFrame(() => {
+ for (let w of [frameCapWidget, frameSkipWidget, pathWidget, skipWidget]) {
+ w.callback(w.value, null, this);
+ }
+ });
+ });
+}
+
+function path_stem(path) {
+ let i = path.lastIndexOf("/");
+ if (i >= 0) {
+ return [path.slice(0,i+1),path.slice(i+1)];
+ }
+ return ["",path];
+}
+function searchBox(event, [x,y], node) {
+ //Ensure only one dialogue shows at a time
+ if (this.prompt)
+ return;
+ this.prompt = true;
+
+ let pathWidget = this;
+ let dialog = document.createElement("div");
+ dialog.className = "litegraph litesearchbox graphdialog rounded"
+ dialog.innerHTML = 'Path '
+ dialog.close = () => {
+ dialog.remove();
+ }
+ document.body.append(dialog);
+ if (app.canvas.ds.scale > 1) {
+ dialog.style.transform = "scale(" + app.canvas.ds.scale + ")";
+ }
+ var name_element = dialog.querySelector(".name");
+ var input = dialog.querySelector(".value");
+ var options_element = dialog.querySelector(".helper");
+ input.value = pathWidget.value;
+
+ var timeout = null;
+ let last_path = null;
+
+ input.addEventListener("keydown", (e) => {
+ dialog.is_modified = true;
+ if (e.keyCode == 27) {
+ //ESC
+ dialog.close();
+ } else if (e.keyCode == 13 && e.target.localName != "textarea") {
+ pathWidget.value = input.value;
+ if (pathWidget.callback) {
+ pathWidget.callback(pathWidget.value);
+ }
+ dialog.close();
+ } else {
+ if (e.keyCode == 9) {
+ //TAB
+ input.value = last_path + options_element.firstChild.innerText;
+ e.preventDefault();
+ e.stopPropagation();
+ } else if (e.ctrlKey && e.keyCode == 87) {
+ //Ctrl+w
+ //most browsers won't support, but it's good QOL for those that do
+ input.value = path_stem(input.value.slice(0,-1))[0]
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ timeout = setTimeout(updateOptions, 10);
+ return;
+ }
+ this.prompt=false;
+ e.preventDefault();
+ e.stopPropagation();
+ });
+
+ var button = dialog.querySelector("button");
+ button.addEventListener("click", (e) => {
+ pathWidget.value = input.value;
+ if (pathWidget.callback) {
+ pathWidget.callback(pathWidget.value);
+ }
+ //unsure why dirty is set here, but not on enter-key above
+ node.graph.setDirtyCanvas(true);
+ dialog.close();
+ this.prompt = false;
+ });
+ var rect = app.canvas.canvas.getBoundingClientRect();
+ var offsetx = -20;
+ var offsety = -20;
+ if (rect) {
+ offsetx -= rect.left;
+ offsety -= rect.top;
+ }
+
+ if (event) {
+ dialog.style.left = event.clientX + offsetx + "px";
+ dialog.style.top = event.clientY + offsety + "px";
+ } else {
+ dialog.style.left = canvas.width * 0.5 + offsetx + "px";
+ dialog.style.top = canvas.height * 0.5 + offsety + "px";
+ }
+ //Search code
+ let options = []
+ function addResult(name, isDir) {
+ let el = document.createElement("div");
+ el.innerText = name;
+ el.className = "litegraph lite-search-item";
+ if (isDir) {
+ el.className += " is-dir";
+ el.addEventListener("click", (e) => {
+ input.value = last_path+name
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ timeout = setTimeout(updateOptions, 10);
+ });
+ } else {
+ el.addEventListener("click", (e) => {
+ pathWidget.value = last_path+name;
+ if (pathWidget.callback) {
+ pathWidget.callback(pathWidget.value);
+ }
+ dialog.close();
+ pathWidget.prompt = false;
+ });
+ }
+ options_element.appendChild(el);
+ }
+ async function updateOptions() {
+ timeout = null;
+ let [path, remainder] = path_stem(input.value);
+ if (last_path != path) {
+ //fetch options. Must block execution here, so update should be async?
+ let params = {path : path, extensions : pathWidget.options.extensions}
+ let optionsURL = api.apiURL('getpath?' + new URLSearchParams(params));
+ try {
+ let resp = await fetch(optionsURL);
+ options = await resp.json();
+ } catch(e) {
+ options = []
+ }
+ last_path = path;
+ }
+ options_element.innerHTML = '';
+ //filter options based on remainder
+ for (let option of options) {
+ if (option.startsWith(remainder)) {
+ let isDir = option.endsWith('/')
+ addResult(option, isDir);
+ }
+ }
+ }
+
+ setTimeout(async function() {
+ input.focus();
+ await updateOptions();
+ }, 10);
+
+ return dialog;
+}
+
+app.ui.settings.addSetting({
+ id: "VHS.AdvancedPreviews",
+ name: "🎥🅥🅗🅢 Advanced Previews",
+ type: "boolean",
+ defaultValue: false,
+});
+
+app.registerExtension({
+ name: "VideoHelperSuite.Core",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if(nodeData?.name?.startsWith("VHS_")) {
+ useKVState(nodeType);
+ chainCallback(nodeType.prototype, "onNodeCreated", function () {
+ let new_widgets = []
+ if (this.widgets) {
+ for (let w of this.widgets) {
+ let input = this.constructor.nodeData.input
+ let config = input?.required[w.name] ?? input.optional[w.name]
+ if (!config) {
+ continue
+ }
+ if (w?.type == "text" && config[1].vhs_path_extensions) {
+ new_widgets.push(app.widgets.VHSPATH({}, w.name, ["VHSPATH", config[1]]));
+ } else {
+ new_widgets.push(w)
+ }
+ }
+ this.widgets = new_widgets;
+ }
+ });
+ }
+ if (nodeData?.name == "VHS_LoadImages") {
+ addUploadWidget(nodeType, nodeData, "directory", "folder");
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ const pathWidget = this.widgets.find((w) => w.name === "directory");
+ chainCallback(pathWidget, "callback", (value) => {
+ if (!value) {
+ return;
+ }
+ let params = {filename : value, type : "input", format: "folder"};
+ this.updateParameters(params, true);
+ });
+ });
+ addLoadImagesCommon(nodeType, nodeData);
+ } else if (nodeData?.name == "VHS_LoadImagesPath") {
+ addUploadWidget(nodeType, nodeData, "directory", "folder");
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ const pathWidget = this.widgets.find((w) => w.name === "directory");
+ chainCallback(pathWidget, "callback", (value) => {
+ if (!value) {
+ return;
+ }
+ let params = {filename : value, type : "path", format: "folder"};
+ this.updateParameters(params, true);
+ });
+ });
+ addLoadImagesCommon(nodeType, nodeData);
+ } else if (nodeData?.name == "VHS_LoadVideo") {
+ addUploadWidget(nodeType, nodeData, "video");
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ const pathWidget = this.widgets.find((w) => w.name === "video");
+ chainCallback(pathWidget, "callback", (value) => {
+ if (!value) {
+ return;
+ }
+ let parts = ["input", value];
+ let extension_index = parts[1].lastIndexOf(".");
+ let extension = parts[1].slice(extension_index+1);
+ let format = "video"
+ if (["gif", "webp", "avif"].includes(extension)) {
+ format = "image"
+ }
+ format += "/" + extension;
+ let params = {filename : parts[1], type : parts[0], format: format};
+ this.updateParameters(params, true);
+ });
+ });
+ addLoadVideoCommon(nodeType, nodeData);
+ } else if (nodeData?.name =="VHS_LoadVideoPath") {
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ const pathWidget = this.widgets.find((w) => w.name === "video");
+ chainCallback(pathWidget, "callback", (value) => {
+ let extension_index = value.lastIndexOf(".");
+ let extension = value.slice(extension_index+1);
+ let format = "video"
+ if (["gif", "webp", "avif"].includes(extension)) {
+ format = "image"
+ }
+ format += "/" + extension;
+ let params = {filename : value, type: "path", format: format};
+ this.updateParameters(params, true);
+ });
+ });
+ addLoadVideoCommon(nodeType, nodeData);
+ } else if (nodeData?.name == "VHS_VideoCombine") {
+ addDateFormatting(nodeType, "filename_prefix");
+ chainCallback(nodeType.prototype, "onExecuted", function(message) {
+ if (message?.gifs) {
+ this.updateParameters(message.gifs[0], true);
+ }
+ });
+ addVideoPreview(nodeType);
+ addPreviewOptions(nodeType);
+ addFormatWidgets(nodeType);
+
+ //Hide the information passing 'gif' output
+ //TODO: check how this is implemented for save image
+ chainCallback(nodeType.prototype, "onNodeCreated", function() {
+ this._outputs = this.outputs
+ Object.defineProperty(this, "outputs", {
+ set : function(value) {
+ this._outputs = value;
+ requestAnimationFrame(() => {
+ if (app.nodeOutputs[this.id + ""]) {
+ this.updateParameters(app.nodeOutputs[this.id+""].gifs[0], true);
+ }
+ })
+ },
+ get : function() {
+ return this._outputs;
+ }
+ });
+ //Display previews after reload/ loading workflow
+ requestAnimationFrame(() => {this.updateParameters({}, true);});
+ });
+ } else if (nodeData?.name == "VHS_SaveImageSequence") {
+ //Disabled for safety as VHS_SaveImageSequence is not currently merged
+ //addDateFormating(nodeType, "directory_name", timestamp_widget=true);
+ //addTimestampWidget(nodeType, nodeData, "directory_name")
+ }
+ },
+ async getCustomWidgets() {
+ return {
+ VHSPATH(node, inputName, inputData) {
+ let w = {
+ name : inputName,
+ type : "VHS.PATH",
+ value : "",
+ draw : function(ctx, node, widget_width, y, H) {
+ //Adapted from litegraph.core.js:drawNodeWidgets
+ var show_text = app.canvas.ds.scale > 0.5;
+ var margin = 15;
+ var text_color = LiteGraph.WIDGET_TEXT_COLOR;
+ var secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
+ ctx.textAlign = "left";
+ ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
+ ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
+ ctx.beginPath();
+ if (show_text)
+ ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]);
+ else
+ ctx.rect( margin, y, widget_width - margin * 2, H );
+ ctx.fill();
+ if (show_text) {
+ if(!this.disabled)
+ ctx.stroke();
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(margin, y, widget_width - margin * 2, H);
+ ctx.clip();
+
+ //ctx.stroke();
+ ctx.fillStyle = secondary_text_color;
+ const label = this.label || this.name;
+ if (label != null) {
+ ctx.fillText(label, margin * 2, y + H * 0.7);
+ }
+ ctx.fillStyle = text_color;
+ ctx.textAlign = "right";
+ let disp_text = this.format_path(String(this.value))
+ ctx.fillText(disp_text, widget_width - margin * 2, y + H * 0.7); //30 chars max
+ ctx.restore();
+ }
+ },
+ mouse : searchBox,
+ options : {},
+ format_path : function(path) {
+ //Formats the full path to be under 30 characters
+ if (path.length <= 30) {
+ return path;
+ }
+ let filename = path_stem(path)[1]
+ if (filename.length > 28) {
+ //may all fit, but can't squeeze more info
+ return filename.substr(0,30);
+ }
+ //TODO: find solution for windows, path[1] == ':'?
+ let isAbs = path[0] == '/';
+ let partial = path.substr(path.length - (isAbs ? 28:29))
+ let cutoff = partial.indexOf('/');
+ if (cutoff < 0) {
+ //Can occur, but there isn't a nicer way to format
+ return path.substr(path.length-30);
+ }
+ return (isAbs ? '/…':'…') + partial.substr(cutoff);
+
+ }
+ };
+ if (inputData.length > 1) {
+ if (inputData[1].vhs_path_extensions) {
+ w.options.extensions = inputData[1].vhs_path_extensions;
+ }
+ if (inputData[1].default) {
+ w.value = inputData[1].default;
+ }
+ }
+
+ if (!node.widgets) {
+ node.widgets = [];
+ }
+ node.widgets.push(w);
+ return w;
+ }
+ }
+ }
+});
diff --git a/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/web/js/videoinfo.js b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/web/js/videoinfo.js
new file mode 100644
index 0000000000000000000000000000000000000000..1947b47e21319268be0b614e80dc85b0da5b505b
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI-VideoHelperSuite/web/js/videoinfo.js
@@ -0,0 +1,102 @@
+import { app } from '../../../scripts/app.js'
+
+
+function getVideoMetadata(file) {
+ return new Promise((r) => {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const videoData = new Uint8Array(event.target.result);
+ const dataView = new DataView(videoData.buffer);
+
+ let decoder = new TextDecoder();
+ // Check for known valid magic strings
+ if (dataView.getUint32(0) == 0x1A45DFA3) {
+ //webm
+ //see http://wiki.webmproject.org/webm-metadata/global-metadata
+ //and https://www.matroska.org/technical/elements.html
+ //contrary to specs, tag seems consistently at start
+ //COMMENT + 0x4487 + packed length?
+ //length 0x8d8 becomes 0x48d8
+ //
+ //description for variable length ints https://github.com/ietf-wg-cellar/ebml-specification/blob/master/specification.markdown
+ let offset = 4 + 8; //COMMENT is 7 chars + 1 to realign
+ while(offset < videoData.length-16) {
+ //Check for text tags
+ if (dataView.getUint16(offset) == 0x4487) {
+ //check that name of tag is COMMENT
+ const name = String.fromCharCode(...videoData.slice(offset-7,offset));
+ if (name === "COMMENT") {
+ let vint = dataView.getUint32(offset+2);
+ let n_octets = Math.clz32(vint)+1;
+ if (n_octets < 4) {//250MB sanity cutoff
+ let length = (vint >> (8*(4-n_octets))) & ~(1 << (7*n_octets));
+ const content = decoder.decode(videoData.slice(offset+2+n_octets, offset+2+n_octets+length));
+ const json = JSON.parse(content);
+ r(json);
+ return;
+ }
+ }
+ }
+ offset+=1;
+ }
+ } else if (dataView.getUint32(4) == 0x66747970 && dataView.getUint32(8) == 0x69736F6D) {
+ //mp4
+ //see https://developer.apple.com/documentation/quicktime-file-format
+ //Seems to make no guarantee for alignment
+ let offset = videoData.length-4;
+ while (offset > 16) {//rough safe guess
+ if (dataView.getUint32(offset) == 0x64617461) {//any data tag
+ if (dataView.getUint32(offset - 8) == 0xa9636d74) {//cmt data tag
+ let type = dataView.getUint32(offset+4); //seemingly 1
+ let locale = dataView.getUint32(offset+8); //seemingly 0
+ let size = dataView.getUint32(offset-4) - 4*4;
+ const content = decoder.decode(videoData.slice(offset+12, offset+12+size));
+ const json = JSON.parse(content);
+ r(json);
+ return;
+ }
+ }
+
+ offset-=1;
+ }
+ } else {
+ console.error("Unknown magic: " + dataView.getUint32(0))
+ r();
+ return;
+ }
+
+ };
+
+ reader.readAsArrayBuffer(file);
+ });
+}
+function isVideoFile(file) {
+ if (file?.name?.endsWith(".webm")) {
+ return true;
+ }
+ if (file?.name?.endsWith(".mp4")) {
+ return true;
+ }
+
+ return false;
+}
+
+let originalHandleFile = app.handleFile;
+app.handleFile = handleFile;
+async function handleFile(file) {
+ if (file?.type?.startsWith("video/") || isVideoFile(file)) {
+ const videoInfo = await getVideoMetadata(file);
+ if (videoInfo) {
+ if (videoInfo.workflow) {
+
+ app.loadGraphData(videoInfo.workflow);
+ }
+ //Potentially check for/parse A1111 metadata here.
+ }
+ } else {
+ return await originalHandleFile.apply(this, arguments);
+ }
+}
+
+//hijack comfy-file-input to allow webm/mp4
+document.getElementById("comfy-file-input").accept += ",video/webm,video/mp4";
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/.gitignore b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..a57d6b3f0a04d1a311136cebe0675ad142b84465
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/.gitignore
@@ -0,0 +1,3 @@
+config.ini
+nsp_pantry.json
+__pycache__
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/LICENSE b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..f288702d2fa16d3cdf0035b15a9fcbc552cd88e7
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/README.md b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c690684ddb17657fd706c0c1c867a1141f7041ad
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/README.md
@@ -0,0 +1,312 @@
+# tinyterraNodes
+
+A selection of custom nodes for [ComfyUI](https://github.com/comfyanonymous/ComfyUI).
+
+[](buymeacoffee.com/tinyterra)
+
+
+
+
+
+
+## Installation
+Navigate to the **_ComfyUI/custom_nodes_** directory with cmd, and run:
+
+`git clone https://github.com/TinyTerra/ComfyUI_tinyterraNodes.git`
+
+### Special Features
+**Fullscreen Image Viewer**
+
+*Enabled by default*
+
++ Adds 'Fullscreen (ttN)' to the node right-click context menu Opens a Fullscreen image viewer - containing all images generated by the selected node during the current comfy session.
++ Adds 'Set Default Fullscreen Node (ttN)' to the node right-click context menu Sets the currently selected node as the default Fullscreen node
++ Adds 'Clear Default Fullscreen Node (ttN)' to the node right-click context menu Clears the assigned default Fullscreen node
+
+
++ Slideshow Mode
+ + Toggled On - Automatically jumps to New images as they are generated (Black Background)
+ + Toggled Off - Holds to the current user selected image (Light Background)
++ Fullscreen Overlay
+ + Toggles display of a navigable preview of all the selected nodes images
+ + Toggles display of the default comfy menu
+
++ *Shortcuts*
+ + 'shift + up arrow' => _Open ttN-Fullscreen using selected node OR default fullscreen node_
+
++ *Shortcuts in Fullscreen*
+ + 'up arrow' => _Toggle Fullscreen Overlay_
+
+ + 'down arrow' => _Toggle Slideshow Mode_
+ + 'left arrow' => _Select Image to the left_
+ + 'shift + left arrow' => _Select Image 5 to the left_
+ + 'ctrl + left arrow' => _Select the first Image_
+ + 'Right arrow' => _Select Image to the right_
+ + 'shift + right arrow' => _Select Image 5 to the right_
+ + 'ctrl + right arrow' => _Select last Image_
+ + 'mouse scroll' => _Select image to Left/Right_
+ + 'esc' => _Close Fullscreen Mode_
+
+**Embedding Auto Complete**
+
+*Enabled by default*
++ displays a popup to autocomplete embedding filenames in text widgets - to use, start typing **embedding** and select an option from the list
++ Option to disable ([ttNodes] enable_embed_autocomplete = True | False)
+
+**Dynamic Widgets**
+
+*Enabled by default*
+
++ Automatically hides and shows widgets depending on their relevancy
++ Option to disable ([ttNodes] enable_dynamic_widgets = True | False)
+
+**ttNinterface**
+
+*Enabled by default*
+
++ Adds 'Node Dimensions (ttN)' to the node right-click context menu Allows setting specific node Width and Height values as long as they are above the minimum size for the given node.
++ Adds 'Default BG Color (ttN)' to the node right-click context menu Allows setting specific default background color for every node added.
++ Adds 'Show Execution Order (ttN)' to the node right-click context menu Toggles execution order flags on node corners.
++ Adds support for 'ctrl + arrow key' Node movement This aligns the node(s) to the set ComfyUI grid spacing size and move the node in the direction of the arrow key by the grid spacing value. Holding shift in addition will move the node by the grid spacing size * 10.
++ Adds 'Reload Node (ttN)' to the node right-click context menu Creates a new instance of the node with the same position, size, color and title (will disconnect any IO wires). It attempts to retain set widget values which is useful for replacing nodes when a node/widget update occurs
++ Adds 'Slot Type Color (ttN)' to the Link right-click context menu Opens a color picker dialog menu to update the color of the selected link type.
++ Adds 'Link Border (ttN)' to the Link right-click context menu Toggles link line border.
++ Adds 'Link Shadow (ttN)' to the Link right-click context menu Toggles link line shadow.
++ Adds 'Link Style (ttN)' to the Link right-click context menu Sets the default link line type.
+
+
+**Save image prefix parsing**
+
++ Add date/time info to filenames or output folder by using: %date:yyyy-MM-dd-hh-mm-ss%
++ Parse any upstream setting into filenames or output folder by using %[widget_name]% (for the current node)
+or %[input_name]>[input_name]>[widget_name]% (for inputting nodes)
+ Example:
+
+
+ 
+
+
+**Node Versioning**
+
++ All tinyterraNodes now have a version property so that if any future changes are made to widgets that would break workflows the nodes will be highlighted on load
++ Will only work with workflows created/saved after the v1.0.0 release
+
+**AutoUpdate**
+
+*Disabled by default*
+
++ Option to auto-update the node pack ([ttNodes] auto_update = False | True)
+
+
+
+ $\Large\color{white}{Nodes}$
+
+## ttN/pipe
+
+
+ pipeLoader
+
+(Modified from [Efficiency Nodes](https://github.com/LucianoCirino/efficiency-nodes-comfyui) and [ADV_CLIP_emb](https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb))
+
+Combination of Efficiency Loader and Advanced CLIP Text Encode with an additional pipe output
++ _**Inputs -** model, vae, clip skip, (lora1, modelstrength clipstrength), (Lora2, modelstrength clipstrength), (Lora3, modelstrength clipstrength), (positive prompt, token normalization, weight interpretation), (negative prompt, token normalization, weight interpretation), (latent width, height), batch size, seed_
++ _**Outputs -** pipe, model, conditioning, conditioning, samples, vae, clip, seed_
+
+
+
+ pipeKSampler
+
+(Modified from [Efficiency Nodes](https://github.com/LucianoCirino/efficiency-nodes-comfyui) and [QOLS_Omar92](https://github.com/omar92/ComfyUI-QualityOfLifeSuit_Omar92))
+
+Combination of Efficiency Loader and Advanced CLIP Text Encode with an additional pipe output
++ _**Inputs -** pipe, (optional pipe overrides), xyplot, (Lora, model strength, clip strength), (upscale method, factor, crop), sampler state, steps, cfg, sampler name, scheduler, denoise, (image output [None, Preview, Save]), Save_Prefix, seed_
++ _**Outputs -** pipe, model, conditioning, conditioning, samples, vae, clip, image, seed_
+
+Old node layout:
+
+
+
+With pipeLoader and pipeKSampler:
+
+
+
+
+
+ pipeKSamplerAdvanced
+
+Combination of Efficiency Loader and Advanced CLIP Text Encode with an additional pipe output
++ _**Inputs -** pipe, (optional pipe overrides), xyplot, (Lora, model strength, clip strength), (upscale method, factor, crop), sampler state, steps, cfg, sampler name, scheduler, starts_at_step, return_with_leftover_noise, (image output [None, Preview, Save]), Save_Prefix_
++ _**Outputs -** pipe, model, conditioning, conditioning, samples, vae, clip, image, seed_
+
+
+
+
+ pipeLoaderSDXL
+
+SDXL Loader and Advanced CLIP Text Encode with an additional pipe output
++ _**Inputs -** model, vae, clip skip, (lora1, modelstrength clipstrength), (Lora2, modelstrength clipstrength), model, vae, clip skip, (lora1, modelstrength clipstrength), (Lora2, modelstrength clipstrength), (positive prompt, token normalization, weight interpretation), (negative prompt, token normalization, weight interpretation), (latent width, height), batch size, seed_
++ _**Outputs -** sdxlpipe, model, conditioning, conditioning, vae, model, conditioning, conditioning, vae, samples, clip, seed_
+
+
+
+ pipeKSamplerSDXL
+
+SDXL Sampler (base and refiner in one) and Advanced CLIP Text Encode with an additional pipe output
++ _**Inputs -** sdxlpipe, (optional pipe overrides), (upscale method, factor, crop), sampler state, base_steps, refiner_steps cfg, sampler name, scheduler, (image output [None, Preview, Save]), Save_Prefix, seed_
++ _**Outputs -** pipe, model, conditioning, conditioning, vae, model, conditioning, conditioning, vae, samples, clip, image, seed_
+
+Old node layout:
+
+
+
+With pipeLoaderSDXL and pipeKSamplerSDXL:
+
+
+
+
+
+ pipeIN
+
+Encode up to 8 frequently used inputs into a single Pipe line.
++ _**Inputs -** model, conditioning, conditioning, samples, vae, clip, image, seed_
++ _**Outputs -** pipe_
+
+
+
+ pipeOUT
+
+Decode single Pipe line into the 8 original outputs, AND a Pipe throughput.
++ _**Inputs -** pipe_
++ _**Outputs -** model, conditioning, conditioning, samples, vae, clip, image, seed, pipe_
+
+
+
+ pipeEDIT
+
+Update/Overwrite any of the 8 original inputs in a Pipe line with new information.
++ _**Inputs -** pipe, model, conditioning, conditioning, samples, vae, clip, image, seed_
++ _**Outputs -** pipe_
+
+
+
+ pipe > basic_pipe
+
+Convert ttN pipe line to basic pipe (to be compatible with [ImpactPack](https://github.com/ltdrdata/ComfyUI-Impact-Pack)), WITH original pipe throughput
++ _**Inputs -** pipe[model, conditioning, conditioning, samples, vae, clip, image, seed]_
++ _**Outputs -** basic_pipe[model, clip, vae, conditioning, conditioning], pipe_
+
+
+
+ pipe > Detailer Pipe
+
+Convert ttN pipe line to detailer pipe (to be compatible with [ImpactPack](https://github.com/ltdrdata/ComfyUI-Impact-Pack)), WITH original pipe throughput
++ _**Inputs -** pipe[model, conditioning, conditioning, samples, vae, clip, image, seed], bbox_detector, sam_model_opt_
++ _**Outputs -** detailer_pipe[model, vae, conditioning, conditioning, bbox_detector, sam_model_opt], pipe_
+
+
+
+ pipe > xyPlot
+
+pipeKSampler input to generate xy plots using sampler and loader values. (Any values not set by xyPlot will be taken from the corresponding pipeKSampler or pipeLoader)
++ _**Inputs -** grid_spacing, latent_id, flip_xy, x_axis, x_values, y_axis, y_values_
++ _**Outputs -** xyPlot_
+
+
+## ttN/image
+
+
+ imageOutput
+
+Preview or Save an image with one node, with image throughput.
++ _**Inputs -** image, image output[Hide, Preview, Save, Hide/Save], output path, save prefix, number padding[None, 2-9], file type[PNG, JPG, JPEG, BMP, TIFF, TIF] overwrite existing[True, False], embed workflow[True, False]_
++ _**Outputs -** image_
+
+
+
+
+ imageRemBG
+
+(Using [RemBG](https://github.com/danielgatis/rembg))
+
+Background Removal node with optional image preview & save.
++ _**Inputs -** image, image output[Disabled, Preview, Save], save prefix_
++ _**Outputs -** image, mask_
+
+Example of a photobashing workflow using pipeNodes, imageRemBG, imageOutput and nodes from [ADV_CLIP_emb](https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb) and [ImpactPack](https://github.com/ltdrdata/ComfyUI-Impact-Pack/tree/Main):
+
+
+
+
+
+ hiresFix
+
+Upscale image by model, optional rescale of result image.
++ _**Inputs -** image, vae, upscale_model, rescale_after_model[true, false], rescale[by_percentage, to Width/Height], rescale method[nearest-exact, bilinear, area], factor, width, height, crop, image_output[Hide, Preview, Save], save prefix, output_latent[true, false]_
++ _**Outputs -** image, latent_
+
+
+## ttN/text
+
+ text
+
+Basic TextBox Loader.
++ _**Outputs -** text (STRING)_
+
+
+
+ textDebug
+
+Text input, to display text inside the node, with optional print to console.
++ _**inputs -** text, print_to_console_
++ _**Outputs -** text (STRING)_
+
+
+
+ textConcat
+
+3 TextBOX inputs with a single concatenated output.
++ _**inputs -** text1, text2, text3 (STRING's), delimiter_
++ _**Outputs -** text (STRING)_
+
+
+
+ 7x TXT Loader Concat
+
+7 TextBOX inputs concatenated with spaces into a single output, AND seperate text outputs.
++ _**inputs -** text1, text2, text3, text4, text5, text6, text7 (STRING's), delimiter_
++ _**Outputs -** text1, text2, text3, text4, text5, text6, text7, concat (STRING's)_
+
+
+
+ 3x TXT Loader MultiConcat
+
+3 TextBOX inputs with seperate text outputs AND multiple concatenation variations (concatenated with spaces).
++ _**inputs -** text1, text2, text3 (STRING's), delimiter_
++ _**Outputs -** text1, text2, text3, 1 & 2, 1 & 3, 2 & 3, concat (STRING's)_
+
+
+## ttN/util
+
+ seed
+
+Basic Seed Loader.
++ _**Outputs -** seed (INT)_
+
+
+
+ float
+
+float loader and converter
++ _**inputs -** float (FLOAT)_
++ _**Outputs -** float, int, text (FLOAT, INT, STRING)_
+
+
+
+ int
+
+int loader and converter
++ _**inputs -** int (INT)_
++ _**Outputs -** int, float, text (INT, FLOAT, STRING)_
+
+
+
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__init__.py b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7210fecdc26a52a06ff902f6541158b747b87ca3
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__init__.py
@@ -0,0 +1,209 @@
+from .tinyterraNodes import TTN_VERSIONS
+import configparser
+import folder_paths
+import subprocess
+import shutil
+import sys
+import os
+
+# ------- CONFIG -------- #
+cwd_path = os.path.dirname(os.path.realpath(__file__))
+comfy_path = folder_paths.base_path
+
+config_path = os.path.join(cwd_path, "config.ini")
+
+optionValues = {
+ "auto_update": ('true', 'false'),
+ "install_rembg": ('true', 'false'),
+ "enable_embed_autocomplete": ('true', 'false'),
+ "enable_interface": ('true', 'false'),
+ "enable_fullscreen": ('true', 'false'),
+ "enable_dynamic_widgets": ('true', 'false'),
+ "enable_dev_nodes": ('true', 'false'),
+ }
+
+def get_config():
+ """Return a configparser.ConfigParser object."""
+ config = configparser.ConfigParser()
+ config.read(config_path)
+ return config
+
+def update_config():
+ #section > option > value
+ for node, version in TTN_VERSIONS.items():
+ config_write("Versions", node, version)
+
+ for option, value in optionValues.items():
+ config_write("Option Values", option, value)
+
+ section_data = {
+ "ttNodes": {
+ "auto_update": False,
+ "install_rembg": False,
+ "enable_interface": True,
+ "enable_fullscreen": True,
+ "enable_embed_autocomplete": True,
+ "enable_dynamic_widgets": True,
+ "enable_dev_nodes": False,
+ }
+ }
+
+ for section, data in section_data.items():
+ for option, value in data.items():
+ if config_read(section, option) is None:
+ config_write(section, option, value)
+
+ # Load the configuration data into a dictionary.
+ config_data = config_load()
+
+ # Iterate through the configuration data.
+ for section, options in config_data.items():
+ if section == "Versions":
+ continue
+ for option in options:
+ # If the option is not in `optionValues` or in `section_data`, remove it.
+ if (option not in optionValues and
+ (section not in section_data or option not in section_data[section])):
+ config_remove(section, option)
+
+def config_load():
+ """Load the entire configuration into a dictionary."""
+ config = get_config()
+ return {section: dict(config.items(section)) for section in config.sections()}
+
+def config_read(section, option):
+ """Read a configuration option."""
+ config = get_config()
+ return config.get(section, option, fallback=None)
+
+def config_write(section, option, value):
+ """Write a configuration option."""
+ config = get_config()
+ if not config.has_section(section):
+ config.add_section(section)
+ config.set(section, str(option), str(value))
+
+ with open(config_path, 'w') as f:
+ config.write(f)
+
+def config_remove(section, option):
+ """Remove an option from a section."""
+ config = get_config()
+ if config.has_section(section):
+ config.remove_option(section, option)
+ with open(config_path, 'w') as f:
+ config.write(f)
+
+def copy_to_web(file):
+ """Copy a file to the web extension path."""
+ shutil.copy(file, web_extension_path)
+
+def config_value_validator(section, option, default):
+ value = str(config_read(section, option)).lower()
+ if value not in optionValues[option]:
+ print(f'\033[92m[{section} Config]\033[91m {option} - \'{value}\' not in {optionValues[option]}, reverting to default.\033[0m')
+ config_write(section, option, default)
+ return default
+ else:
+ return value
+
+# Create a config file if not exists
+if not os.path.isfile(config_path):
+ with open(config_path, 'w') as f:
+ pass
+
+update_config()
+
+# Autoupdate if True
+if config_value_validator("ttNodes", "auto_update", 'false') == 'true':
+ try:
+ with subprocess.Popen(["git", "pull"], cwd=cwd_path, stdout=subprocess.PIPE) as p:
+ p.wait()
+ result = p.communicate()[0].decode()
+ if result != "Already up to date.\n":
+ print("\033[92m[t ttNodes Updated t]\033[0m")
+ except:
+ pass
+
+# Install RemBG if True
+
+Install = config_read("ttNodes", "install_rembg")
+if Install in [True, 'true', 'True']:
+ try:
+ from rembg import remove
+ config_write("ttNodes", "install_rembg", 'Already Installed')
+ except ImportError:
+ if Install not in ('Failed to install', 'Installed successfully'):
+ try:
+ print("\033[92m[ttNodes] \033[0;31mREMBG is not installed. Attempting to install...\033[0m")
+ p = subprocess.Popen([sys.executable, "-m", "pip", "install", "rembg[gpu]"])
+ p.wait()
+ print("\033[92m[ttNodes] REMBG[GPU] Installed!\033[0m")
+
+ config_write("ttNodes", "install_rembg", 'Installed successfully')
+ except:
+ try:
+ print("\033[92m[ttNodes] \033[0;31mREMBG[GPU] failed to install. Attempting to install REMBG...\033[0m")
+ p = subprocess.Popen([sys.executable, "-m", "pip", "install", "rembg"])
+ p.wait()
+ print("\033[92m[ttNodes] REMBG Installed!\033[0m")
+ config_write("ttNodes", "install_rembg", 'Installed successfully')
+ except:
+ config_write("ttNodes", "install_rembg", 'Failed to install')
+ print("\033[92m[ttNodes] \033[0;31mFailed to install REMBG.\033[0m")
+
+# --------- WEB ---------- #
+web_extension_path = os.path.join(comfy_path, "web", "extensions", "tinyterraNodes")
+
+ttNstyles_JS_file_web = os.path.join(web_extension_path, "ttNstyles.js")
+
+ttN_JS_file = os.path.join(cwd_path, "js", "ttN.js")
+ttNxyPlot_JS_file = os.path.join(cwd_path, "js", "ttNxyPlot.js")
+ttNembedAC_JS_file = os.path.join(cwd_path, "js", "ttNembedAC.js")
+ttNwidgets_JS_file = os.path.join(cwd_path, "js", "ttNwidgets.js")
+ttNinterface_JS_file = os.path.join(cwd_path, "js", "ttNinterface.js")
+ttNdynamicWidgets_JS_file = os.path.join(cwd_path, "js", "ttNdynamicWidgets.js")
+ttNfullscreen_JS_file = os.path.join(cwd_path, "js", "ttNfullscreen.js")
+
+if not os.path.exists(web_extension_path):
+ os.makedirs(web_extension_path)
+else:
+ shutil.rmtree(web_extension_path)
+ os.makedirs(web_extension_path)
+
+copy_to_web(ttN_JS_file)
+copy_to_web(ttNwidgets_JS_file)
+copy_to_web(ttNxyPlot_JS_file)
+
+# Enable Custom Styles if True
+if config_value_validator("ttNodes", "enable_interface", 'true') == 'true':
+ copy_to_web(ttNinterface_JS_file)
+
+if config_value_validator("ttNodes", "enable_fullscreen", 'true') == 'true':
+ copy_to_web(ttNfullscreen_JS_file)
+
+# Enable Embed Autocomplete if True
+if config_value_validator("ttNodes", "enable_embed_autocomplete", "true") == 'true':
+ copy_to_web(ttNembedAC_JS_file)
+
+# Enable Dynamic Widgets if True
+if config_value_validator("ttNodes", "enable_dynamic_widgets", "true") == 'true':
+ copy_to_web(ttNdynamicWidgets_JS_file)
+
+# Enable Dev Nodes if True
+if config_value_validator("ttNodes", "enable_dev_nodes", 'true') == 'true':
+ ttNbusJSfile = os.path.join(cwd_path, "dev", "ttNbus.js")
+ ttNdebugJSfile = os.path.join(cwd_path, "dev", "ttNdebug.js")
+
+ from .ttNdev import NODE_CLASS_MAPPINGS as ttNdev_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as ttNdev_DISPLAY_NAME_MAPPINGS
+else:
+ ttNdev_CLASS_MAPPINGS = {}
+ ttNdev_DISPLAY_NAME_MAPPINGS = {}
+
+# ------- MAPPING ------- #
+from .tinyterraNodes import NODE_CLASS_MAPPINGS as ttN_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as ttN_DISPLAY_NAME_MAPPINGS
+
+NODE_CLASS_MAPPINGS = {**ttN_CLASS_MAPPINGS, **ttNdev_CLASS_MAPPINGS}
+NODE_DISPLAY_NAME_MAPPINGS = {**ttN_DISPLAY_NAME_MAPPINGS, **ttNdev_DISPLAY_NAME_MAPPINGS}
+
+__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__pycache__/__init__.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a276ab463dd6f8e19174bc8c2fbe9e20ad4e9590
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__pycache__/__init__.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__pycache__/adv_encode.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__pycache__/adv_encode.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1fea596fe19dd0f2f7ca9b0b6ed19762421d741e
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__pycache__/adv_encode.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__pycache__/tinyterraNodes.cpython-310.pyc b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__pycache__/tinyterraNodes.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1336df7aeb9d88ec72cd9d345fdd2e78b9f09d14
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/__pycache__/tinyterraNodes.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/adv_encode.py b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/adv_encode.py
new file mode 100644
index 0000000000000000000000000000000000000000..c6ad2e904e2d15424154ad9ad88775910071a29c
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/adv_encode.py
@@ -0,0 +1,290 @@
+import torch
+import numpy as np
+import itertools
+from math import gcd
+
+from comfy import model_management
+from comfy.sdxl_clip import SDXLClipModel, SDXLRefinerClipModel, SDXLClipG
+
+def _grouper(n, iterable):
+ it = iter(iterable)
+ while True:
+ chunk = list(itertools.islice(it, n))
+ if not chunk:
+ return
+ yield chunk
+
+def _norm_mag(w, n):
+ d = w - 1
+ return 1 + np.sign(d) * np.sqrt(np.abs(d)**2 / n)
+ #return np.sign(w) * np.sqrt(np.abs(w)**2 / n)
+
+def divide_length(word_ids, weights):
+ sums = dict(zip(*np.unique(word_ids, return_counts=True)))
+ sums[0] = 1
+ weights = [[_norm_mag(w, sums[id]) if id != 0 else 1.0
+ for w, id in zip(x, y)] for x, y in zip(weights, word_ids)]
+ return weights
+
+def shift_mean_weight(word_ids, weights):
+ delta = 1 - np.mean([w for x, y in zip(weights, word_ids) for w, id in zip(x,y) if id != 0])
+ weights = [[w if id == 0 else w+delta
+ for w, id in zip(x, y)] for x, y in zip(weights, word_ids)]
+ return weights
+
+def scale_to_norm(weights, word_ids, w_max):
+ top = np.max(weights)
+ w_max = min(top, w_max)
+ weights = [[w_max if id == 0 else (w/top) * w_max
+ for w, id in zip(x, y)] for x, y in zip(weights, word_ids)]
+ return weights
+
+def from_zero(weights, base_emb):
+ weight_tensor = torch.tensor(weights, dtype=base_emb.dtype, device=base_emb.device)
+ weight_tensor = weight_tensor.reshape(1,-1,1).expand(base_emb.shape)
+ return base_emb * weight_tensor
+
+def mask_word_id(tokens, word_ids, target_id, mask_token):
+ new_tokens = [[mask_token if wid == target_id else t
+ for t, wid in zip(x,y)] for x,y in zip(tokens, word_ids)]
+ mask = np.array(word_ids) == target_id
+ return (new_tokens, mask)
+
+def batched_clip_encode(tokens, length, encode_func, num_chunks):
+ embs = []
+ for e in _grouper(32, tokens):
+ enc, pooled = encode_func(e)
+ enc = enc.reshape((len(e), length, -1))
+ embs.append(enc)
+ embs = torch.cat(embs)
+ embs = embs.reshape((len(tokens) // num_chunks, length * num_chunks, -1))
+ return embs
+
+def from_masked(tokens, weights, word_ids, base_emb, length, encode_func, m_token=266):
+ pooled_base = base_emb[0,length-1:length,:]
+ wids, inds = np.unique(np.array(word_ids).reshape(-1), return_index=True)
+ weight_dict = dict((id,w)
+ for id,w in zip(wids ,np.array(weights).reshape(-1)[inds])
+ if w != 1.0)
+
+ if len(weight_dict) == 0:
+ return torch.zeros_like(base_emb), base_emb[0,length-1:length,:]
+
+ weight_tensor = torch.tensor(weights, dtype=base_emb.dtype, device=base_emb.device)
+ weight_tensor = weight_tensor.reshape(1,-1,1).expand(base_emb.shape)
+
+ #m_token = (clip.tokenizer.end_token, 1.0) if clip.tokenizer.pad_with_end else (0,1.0)
+ #TODO: find most suitable masking token here
+ m_token = (m_token, 1.0)
+
+ ws = []
+ masked_tokens = []
+ masks = []
+
+ #create prompts
+ for id, w in weight_dict.items():
+ masked, m = mask_word_id(tokens, word_ids, id, m_token)
+ masked_tokens.extend(masked)
+
+ m = torch.tensor(m, dtype=base_emb.dtype, device=base_emb.device)
+ m = m.reshape(1,-1,1).expand(base_emb.shape)
+ masks.append(m)
+
+ ws.append(w)
+
+ #batch process prompts
+ embs = batched_clip_encode(masked_tokens, length, encode_func, len(tokens))
+ masks = torch.cat(masks)
+
+ embs = (base_emb.expand(embs.shape) - embs)
+ pooled = embs[0,length-1:length,:]
+
+ embs *= masks
+ embs = embs.sum(axis=0, keepdim=True)
+
+ pooled_start = pooled_base.expand(len(ws), -1)
+ ws = torch.tensor(ws).reshape(-1,1).expand(pooled_start.shape)
+ pooled = (pooled - pooled_start) * (ws - 1)
+ pooled = pooled.mean(axis=0, keepdim=True)
+
+ return ((weight_tensor - 1) * embs), pooled_base + pooled
+
+def mask_inds(tokens, inds, mask_token):
+ clip_len = len(tokens[0])
+ inds_set = set(inds)
+ new_tokens = [[mask_token if i*clip_len + j in inds_set else t
+ for j, t in enumerate(x)] for i, x in enumerate(tokens)]
+ return new_tokens
+
+def down_weight(tokens, weights, word_ids, base_emb, length, encode_func, m_token=266):
+ w, w_inv = np.unique(weights,return_inverse=True)
+
+ if np.sum(w < 1) == 0:
+ return base_emb, tokens, base_emb[0,length-1:length,:]
+ #m_token = (clip.tokenizer.end_token, 1.0) if clip.tokenizer.pad_with_end else (0,1.0)
+ #using the comma token as a masking token seems to work better than aos tokens for SD 1.x
+ m_token = (m_token, 1.0)
+
+ masked_tokens = []
+
+ masked_current = tokens
+ for i in range(len(w)):
+ if w[i] >= 1:
+ continue
+ masked_current = mask_inds(masked_current, np.where(w_inv == i)[0], m_token)
+ masked_tokens.extend(masked_current)
+
+ embs = batched_clip_encode(masked_tokens, length, encode_func, len(tokens))
+ embs = torch.cat([base_emb, embs])
+ w = w[w<=1.0]
+ w_mix = np.diff([0] + w.tolist())
+ w_mix = torch.tensor(w_mix, dtype=embs.dtype, device=embs.device).reshape((-1,1,1))
+
+ weighted_emb = (w_mix * embs).sum(axis=0, keepdim=True)
+ return weighted_emb, masked_current, weighted_emb[0,length-1:length,:]
+
+def scale_emb_to_mag(base_emb, weighted_emb):
+ norm_base = torch.linalg.norm(base_emb)
+ norm_weighted = torch.linalg.norm(weighted_emb)
+ embeddings_final = (norm_base / norm_weighted) * weighted_emb
+ return embeddings_final
+
+def recover_dist(base_emb, weighted_emb):
+ fixed_std = (base_emb.std() / weighted_emb.std()) * (weighted_emb - weighted_emb.mean())
+ embeddings_final = fixed_std + (base_emb.mean() - fixed_std.mean())
+ return embeddings_final
+
+def A1111_renorm(base_emb, weighted_emb):
+ embeddings_final = (base_emb.mean() / weighted_emb.mean()) * weighted_emb
+ return embeddings_final
+
+def advanced_encode_from_tokens(tokenized, token_normalization, weight_interpretation, encode_func, m_token=266, length=77, w_max=1.0, return_pooled=False, apply_to_pooled=False):
+ tokens = [[t for t,_,_ in x] for x in tokenized]
+ weights = [[w for _,w,_ in x] for x in tokenized]
+ word_ids = [[wid for _,_,wid in x] for x in tokenized]
+
+ #weight normalization
+ #====================
+
+ #distribute down/up weights over word lengths
+ if token_normalization.startswith("length"):
+ weights = divide_length(word_ids, weights)
+
+ #make mean of word tokens 1
+ if token_normalization.endswith("mean"):
+ weights = shift_mean_weight(word_ids, weights)
+
+ #weight interpretation
+ #=====================
+ pooled = None
+
+ if weight_interpretation == "comfy":
+ weighted_tokens = [[(t,w) for t, w in zip(x, y)] for x, y in zip(tokens, weights)]
+ weighted_emb, pooled_base = encode_func(weighted_tokens)
+ pooled = pooled_base
+ else:
+ unweighted_tokens = [[(t,1.0) for t, _,_ in x] for x in tokenized]
+ base_emb, pooled_base = encode_func(unweighted_tokens)
+
+ if weight_interpretation == "A1111":
+ weighted_emb = from_zero(weights, base_emb)
+ weighted_emb = A1111_renorm(base_emb, weighted_emb)
+ pooled = pooled_base
+
+ if weight_interpretation == "compel":
+ pos_tokens = [[(t,w) if w >= 1.0 else (t,1.0) for t, w in zip(x, y)] for x, y in zip(tokens, weights)]
+ weighted_emb, _ = encode_func(pos_tokens)
+ weighted_emb, _, pooled = down_weight(pos_tokens, weights, word_ids, weighted_emb, length, encode_func)
+
+ if weight_interpretation == "comfy++":
+ weighted_emb, tokens_down, _ = down_weight(unweighted_tokens, weights, word_ids, base_emb, length, encode_func)
+ weights = [[w if w > 1.0 else 1.0 for w in x] for x in weights]
+ #unweighted_tokens = [[(t,1.0) for t, _,_ in x] for x in tokens_down]
+ embs, pooled = from_masked(unweighted_tokens, weights, word_ids, base_emb, length, encode_func)
+ weighted_emb += embs
+
+ if weight_interpretation == "down_weight":
+ weights = scale_to_norm(weights, word_ids, w_max)
+ weighted_emb, _, pooled = down_weight(unweighted_tokens, weights, word_ids, base_emb, length, encode_func)
+
+ if return_pooled:
+ if apply_to_pooled:
+ return weighted_emb, pooled
+ else:
+ return weighted_emb, pooled_base
+ return weighted_emb, None
+
+def encode_token_weights_g(model, token_weight_pairs):
+ return model.clip_g.encode_token_weights(token_weight_pairs)
+
+def encode_token_weights_l(model, token_weight_pairs):
+ l_out, _ = model.clip_l.encode_token_weights(token_weight_pairs)
+ return l_out, None
+
+def encode_token_weights(model, token_weight_pairs, encode_func):
+ if model.layer_idx is not None:
+ model.cond_stage_model.clip_layer(model.layer_idx)
+
+ model_management.load_model_gpu(model.patcher)
+ return encode_func(model.cond_stage_model, token_weight_pairs)
+
+def prepareXL(embs_l, embs_g, pooled, clip_balance):
+ l_w = 1 - max(0, clip_balance - .5) * 2
+ g_w = 1 - max(0, .5 - clip_balance) * 2
+ if embs_l is not None:
+ return torch.cat([embs_l * l_w, embs_g * g_w], dim=-1), pooled
+ else:
+ return embs_g, pooled
+
+def advanced_encode(clip, text, token_normalization, weight_interpretation, w_max=1.0, clip_balance=.5, apply_to_pooled=True):
+ tokenized = clip.tokenize(text, return_word_ids=True)
+ if isinstance(clip.cond_stage_model, (SDXLClipModel, SDXLRefinerClipModel, SDXLClipG)):
+ embs_l = None
+ embs_g = None
+ pooled = None
+ if 'l' in tokenized and isinstance(clip.cond_stage_model, SDXLClipModel):
+ embs_l, _ = advanced_encode_from_tokens(tokenized['l'],
+ token_normalization,
+ weight_interpretation,
+ lambda x: encode_token_weights(clip, x, encode_token_weights_l),
+ w_max=w_max,
+ return_pooled=False)
+ if 'g' in tokenized:
+ embs_g, pooled = advanced_encode_from_tokens(tokenized['g'],
+ token_normalization,
+ weight_interpretation,
+ lambda x: encode_token_weights(clip, x, encode_token_weights_g),
+ w_max=w_max,
+ return_pooled=True,
+ apply_to_pooled=apply_to_pooled)
+ return prepareXL(embs_l, embs_g, pooled, clip_balance)
+ else:
+ return advanced_encode_from_tokens(tokenized['l'],
+ token_normalization,
+ weight_interpretation,
+ lambda x: (clip.encode_from_tokens({'l': x}), None),
+ w_max=w_max)
+def advanced_encode_XL(clip, text1, text2, token_normalization, weight_interpretation, w_max=1.0, clip_balance=.5, apply_to_pooled=True):
+ tokenized1 = clip.tokenize(text1, return_word_ids=True)
+ tokenized2 = clip.tokenize(text2, return_word_ids=True)
+
+ embs_l, _ = advanced_encode_from_tokens(tokenized1['l'],
+ token_normalization,
+ weight_interpretation,
+ lambda x: encode_token_weights(clip, x, encode_token_weights_l),
+ w_max=w_max,
+ return_pooled=False)
+
+ embs_g, pooled = advanced_encode_from_tokens(tokenized2['g'],
+ token_normalization,
+ weight_interpretation,
+ lambda x: encode_token_weights(clip, x, encode_token_weights_g),
+ w_max=w_max,
+ return_pooled=True,
+ apply_to_pooled=apply_to_pooled)
+
+ gcd_num = gcd(embs_l.shape[1], embs_g.shape[1])
+ repeat_l = int((embs_g.shape[1] / gcd_num) * embs_l.shape[1])
+ repeat_g = int((embs_l.shape[1] / gcd_num) * embs_g.shape[1])
+
+ return prepareXL(embs_l.expand((-1,repeat_l,-1)), embs_g.expand((-1,repeat_g,-1)), pooled, clip_balance)
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/arial.ttf b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/arial.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..ff0815cd8c64b0a245ec780eb8d21867509155b5
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/arial.ttf differ
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/config.ini b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/config.ini
new file mode 100644
index 0000000000000000000000000000000000000000..5e1f3095a3f84065830fb5ea7c3e386176077777
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/config.ini
@@ -0,0 +1,46 @@
+[Versions]
+tinyterranodes = 1.2.0
+pipeloader = 1.1.2
+pipeksampler = 1.0.5
+pipeksampleradvanced = 1.0.5
+pipeloadersdxl = 1.1.2
+pipeksamplersdxl = 1.0.2
+pipein = 1.1.0
+pipeout = 1.1.0
+pipeedit = 1.1.1
+pipe2basic = 1.1.0
+pipe2detailer = 1.2.0
+xyplot = 1.2.0
+pipeencodeconcat = 1.0.2
+multilorastack = 1.1.1
+multimodelmerge = 1.0.1
+text = 1.0.0
+textdebug = 1.0.
+concat = 1.0.0
+text3box_3wayconcat = 1.0.0
+text7box_concat = 1.0.0
+imageoutput = 1.1.0
+imagerembg = 0.0.0
+hiresfixscale = 1.0.3
+int = 1.0.0
+float = 1.0.0
+seed = 1.0.0
+
+[Option Values]
+auto_update = ('true', 'false')
+install_rembg = ('true', 'false')
+enable_embed_autocomplete = ('true', 'false')
+enable_interface = ('true', 'false')
+enable_fullscreen = ('true', 'false')
+enable_dynamic_widgets = ('true', 'false')
+enable_dev_nodes = ('true', 'false')
+
+[ttNodes]
+auto_update = False
+install_rembg = False
+enable_interface = True
+enable_fullscreen = True
+enable_embed_autocomplete = True
+enable_dynamic_widgets = True
+enable_dev_nodes = False
+
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttN.js b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttN.js
new file mode 100644
index 0000000000000000000000000000000000000000..660b8e89878b5f7159233ba68c63e7cd52ff414f
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttN.js
@@ -0,0 +1,571 @@
+import { app } from "../../scripts/app.js";
+import { ComfyWidgets } from "../../scripts/widgets.js";
+
+const CONVERTED_TYPE = "converted-widget";
+const GET_CONFIG = Symbol();
+
+function hideWidget(node, widget, suffix = "") {
+ widget.origType = widget.type;
+ widget.origComputeSize = widget.computeSize;
+ widget.origSerializeValue = widget.serializeValue;
+ widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
+ widget.type = CONVERTED_TYPE + suffix;
+ widget.serializeValue = () => {
+ // Prevent serializing the widget if we have no input linked
+ if (!node.inputs) {
+ return undefined;
+ }
+ let node_input = node.inputs.find((i) => i.widget?.name === widget.name);
+
+ if (!node_input || !node_input.link) {
+ return undefined;
+ }
+ return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
+ };
+
+ // Hide any linked widgets, e.g. seed+seedControl
+ if (widget.linkedWidgets) {
+ for (const w of widget.linkedWidgets) {
+ hideWidget(node, w, ":" + widget.name);
+ }
+ }
+}
+
+function convertToInput(node, widget, config) {
+ console.log('config:', config)
+ hideWidget(node, widget);
+
+ const { type } = getWidgetType(config);
+
+ // Add input and store widget config for creating on primitive node
+ const sz = node.size;
+ node.addInput(widget.name, type, {
+ widget: { name: widget.name, [GET_CONFIG]: () => config },
+ });
+
+ for (const widget of node.widgets) {
+ widget.last_y += LiteGraph.NODE_SLOT_HEIGHT;
+ }
+
+ // Restore original size but grow if needed
+ node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
+}
+
+function getWidgetType(config) {
+ // Special handling for COMBO so we restrict links based on the entries
+ let type = config[0];
+ if (type instanceof Array) {
+ type = "COMBO";
+ }
+ return { type };
+}
+
+app.registerExtension({
+ name: "comfy.ttN",
+ init() {
+ const ttNreloadNode = function (node) {
+ const nodeType = node.constructor.type;
+ const origVals = node.properties.origVals || {};
+
+ const nodeTitle = origVals.title || node.title;
+ const nodeColor = origVals.color || node.color;
+ const bgColor = origVals.bgcolor || node.bgcolor;
+ const oldNode = node
+ const options = {
+ 'size': [...node.size],
+ 'color': nodeColor,
+ 'bgcolor': bgColor,
+ 'pos': [...node.pos]
+ }
+
+ let inputLinks = []
+ let outputLinks = []
+ for (const input of node.inputs) {
+ if (input.link) {
+ const input_name = input.name
+ const input_slot = node.findInputSlot(input_name)
+ const input_node = node.getInputNode(input_slot)
+ const input_link = node.getInputLink(input_slot)
+
+ inputLinks.push([input_link.origin_slot, input_node, input_name])
+ }
+ }
+ for (const output of node.outputs) {
+ if (output.links) {
+ const output_name = output.name
+
+ for (const linkID of output.links) {
+ const output_link = graph.links[linkID]
+ const output_node = graph._nodes_by_id[output_link.target_id]
+ outputLinks.push([output_name, output_node, output_link.target_slot])
+ }
+ }
+ }
+
+ app.graph.remove(node)
+ const newNode = app.graph.add(LiteGraph.createNode(nodeType, nodeTitle, options));
+ if (newNode?.constructor?.hasOwnProperty('ttNnodeVersion')) {
+ newNode.properties.ttNnodeVersion = newNode.constructor.ttNnodeVersion;
+ }
+
+ function handleLinks() {
+ // re-convert inputs
+ for (let w of oldNode.widgets) {
+ if (w.type === 'converted-widget') {
+ const WidgetToConvert = newNode.widgets.find((nw) => nw.name === w.name);
+ for (let i of oldNode.inputs) {
+ if (i.name === w.name) {
+ convertToInput(newNode, WidgetToConvert, i.widget);
+ }
+ }
+ }
+ }
+ // replace input and output links
+ for (let input of inputLinks) {
+ const [output_slot, output_node, input_name] = input;
+ output_node.connect(output_slot, newNode.id, input_name)
+ }
+ for (let output of outputLinks) {
+ const [output_name, input_node, input_slot] = output;
+ newNode.connect(output_name, input_node, input_slot)
+ }
+ }
+
+ // fix widget values
+ let values = oldNode.widgets_values;
+ if (!values) {
+ newNode.widgets.forEach((newWidget, index) => {
+ const oldWidget = oldNode.widgets[index];
+ if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) {
+ newWidget.value = oldWidget.value;
+ }
+ });
+ handleLinks();
+ return;
+ }
+ let pass = false
+ const isIterateForwards = values.length <= newNode.widgets.length;
+ let vi = isIterateForwards ? 0 : values.length - 1;
+ function evalWidgetValues(testValue, newWidg) {
+ if (testValue === true || testValue === false) {
+ if (newWidg.options?.on && newWidg.options?.off) {
+ return { value: testValue, pass: true };
+ }
+ } else if (typeof testValue === "number") {
+ if (newWidg.options?.min <= testValue && testValue <= newWidg.options?.max) {
+ return { value: testValue, pass: true };
+ }
+ } else if (newWidg.options?.values?.includes(testValue)) {
+ return { value: testValue, pass: true };
+ } else if (newWidg.inputEl && typeof testValue === "string") {
+ return { value: testValue, pass: true };
+ }
+ return { value: newWidg.value, pass: false };
+ }
+ const updateValue = (wi) => {
+ const oldWidget = oldNode.widgets[wi];
+ let newWidget = newNode.widgets[wi];
+ if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) {
+ while ((isIterateForwards ? vi < values.length : vi >= 0) && !pass) {
+ let { value, pass } = evalWidgetValues(values[vi], newWidget);
+ if (pass && value !== null) {
+ newWidget.value = value;
+ break;
+ }
+ vi += isIterateForwards ? 1 : -1;
+ }
+ vi++
+ if (!isIterateForwards) {
+ vi = values.length - (newNode.widgets.length - 1 - wi);
+ }
+ }
+ };
+ if (isIterateForwards) {
+ for (let wi = 0; wi < newNode.widgets.length; wi++) {
+ updateValue(wi);
+ }
+ } else {
+ for (let wi = newNode.widgets.length - 1; wi >= 0; wi--) {
+ updateValue(wi);
+ }
+ }
+ handleLinks();
+ };
+
+ const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
+ LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
+ const options = getNodeMenuOptions.apply(this, arguments);
+ node.setDirtyCanvas(true, true);
+
+ options.splice(options.length - 1, 0,
+ {
+ content: "Reload Node (ttN)",
+ callback: () => {
+ var graphcanvas = LGraphCanvas.active_canvas;
+ if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
+ ttNreloadNode(node);
+ } else {
+ for (var i in graphcanvas.selected_nodes) {
+ ttNreloadNode(graphcanvas.selected_nodes[i]);
+ }
+ }
+ }
+ },
+ );
+ return options;
+ };
+ },
+ beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name.startsWith("ttN")) {
+ const origOnConfigure = nodeType.prototype.onConfigure;
+ nodeType.prototype.onConfigure = function () {
+ const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined;
+ let nodeVersion = nodeData.input.hidden?.ttNnodeVersion ? nodeData.input.hidden.ttNnodeVersion : null;
+ nodeType.ttNnodeVersion = nodeVersion;
+ this.properties['ttNnodeVersion'] = this.properties['ttNnodeVersion'] ? this.properties['ttNnodeVersion'] : nodeVersion;
+ if (this.properties['ttNnodeVersion'] !== nodeVersion) {
+ if (!this.properties['origVals']) {
+ this.properties['origVals'] = { bgcolor: this.bgcolor, color: this.color, title: this.title }
+ }
+ this.bgcolor = "#d82129";
+ this.color = "#bd000f";
+ this.title = this.title.includes("Node Version Mismatch") ? this.title : this.title + " - Node Version Mismatch"
+ } else if (this.properties['origVals']) {
+ this.bgcolor = this.properties.origVals.bgcolor;
+ this.color = this.properties.origVals.color;
+ this.title = this.properties.origVals.title;
+ delete this.properties['origVals']
+ }
+ return r;
+ };
+ }
+ if (nodeData.name === "ttN textDebug") {
+ const onNodeCreated = nodeType.prototype.onNodeCreated;
+ nodeType.prototype.onNodeCreated = function () {
+ const r = onNodeCreated?.apply(this, arguments);
+ const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
+ w.inputEl.readOnly = true;
+ w.inputEl.style.opacity = 0.7;
+ return r;
+ };
+
+ const onExecuted = nodeType.prototype.onExecuted;
+ nodeType.prototype.onExecuted = function (message) {
+ onExecuted?.apply(this, arguments);
+
+ for (const widget of this.widgets) {
+ if (widget.type === "customtext"){
+ widget.value = message.text.join('');
+ }
+ }
+
+ this.onResize?.(this.size);
+ };
+ }
+ },
+ nodeCreated(node) {
+ if (node.getTitle() === "pipeLoader") {
+ for (let widget of node.widgets) {
+ if (widget.name === "control_after_generate") {
+ widget.value = "fixed"
+ }
+ }
+ }
+ },
+});
+
+
+// ttN Dropdown
+var styleElement = document.createElement("style");
+const cssCode = `
+.ttN-dropdown, .ttN-nested-dropdown {
+ position: relative;
+ box-sizing: border-box;
+ background-color: #171717;
+ box-shadow: 0 4px 4px rgba(255, 255, 255, .25);
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ z-index: 1000;
+ overflow: visible;
+ max-height: fit-content;
+ max-width: fit-content;
+}
+
+.ttN-dropdown {
+ position: absolute;
+ border-radius: 0;
+}
+
+/* Style for final items */
+.ttN-dropdown li.item, .ttN-nested-dropdown li.item {
+ font-weight: normal;
+ min-width: max-content;
+}
+
+/* Style for folders (parent items) */
+.ttN-dropdown li.folder, .ttN-nested-dropdown li.folder {
+ cursor: default;
+ position: relative;
+ border-right: 3px solid cyan;
+}
+
+.ttN-dropdown li.folder::after, .ttN-nested-dropdown li.folder::after {
+ content: ">";
+ position: absolute;
+ right: 2px;
+ font-weight: normal;
+}
+
+.ttN-dropdown li, .ttN-nested-dropdown li {
+ padding: 4px 10px;
+ cursor: pointer;
+ font-family: system-ui;
+ font-size: 0.7rem;
+ position: relative;
+}
+
+/* Style for nested dropdowns */
+.ttN-nested-dropdown {
+ position: absolute;
+ top: 0;
+ left: 100%;
+ margin: 0;
+ border: none;
+ display: none;
+}
+
+.ttN-dropdown li.selected > .ttN-nested-dropdown,
+.ttN-nested-dropdown li.selected > .ttN-nested-dropdown {
+ display: block;
+ border: none;
+}
+
+.ttN-dropdown li.selected,
+.ttN-nested-dropdown li.selected {
+ background-color: #e5e5e5;
+ border: none;
+}
+`
+styleElement.innerHTML = cssCode
+document.head.appendChild(styleElement);
+
+let activeDropdown = null;
+
+export function ttN_RemoveDropdown() {
+ if (activeDropdown) {
+ activeDropdown.removeEventListeners();
+ activeDropdown.dropdown.remove();
+ activeDropdown = null;
+ }
+}
+
+class Dropdown {
+ constructor(inputEl, suggestions, onSelect, isDict = false) {
+ this.dropdown = document.createElement('ul');
+ this.dropdown.setAttribute('role', 'listbox');
+ this.dropdown.classList.add('ttN-dropdown');
+ this.selectedIndex = -1;
+ this.inputEl = inputEl;
+ this.suggestions = suggestions;
+ this.onSelect = onSelect;
+ this.isDict = isDict;
+
+ this.focusedDropdown = this.dropdown;
+
+ this.buildDropdown();
+
+ this.onKeyDownBound = this.onKeyDown.bind(this);
+ this.onWheelBound = this.onWheel.bind(this);
+ this.onClickBound = this.onClick.bind(this);
+
+ this.addEventListeners();
+ }
+
+ buildDropdown() {
+ if (this.isDict) {
+ this.buildNestedDropdown(this.suggestions, this.dropdown);
+ } else {
+ this.suggestions.forEach((suggestion, index) => {
+ this.addListItem(suggestion, index, this.dropdown);
+ });
+ }
+
+ const inputRect = this.inputEl.getBoundingClientRect();
+ this.dropdown.style.top = (inputRect.top + inputRect.height - 10) + 'px';
+ this.dropdown.style.left = inputRect.left + 'px';
+
+ document.body.appendChild(this.dropdown);
+ activeDropdown = this;
+ }
+
+ buildNestedDropdown(dictionary, parentElement) {
+ let index = 0;
+ Object.keys(dictionary).forEach((key) => {
+ const item = dictionary[key];
+ if (typeof item === "object" && item !== null) {
+ const nestedDropdown = document.createElement('ul');
+ nestedDropdown.setAttribute('role', 'listbox');
+ nestedDropdown.classList.add('ttN-nested-dropdown');
+ const parentListItem = document.createElement('li');
+ parentListItem.classList.add('folder');
+ parentListItem.textContent = key;
+ parentListItem.appendChild(nestedDropdown);
+ parentListItem.addEventListener('mouseover', this.onMouseOver.bind(this, index, parentElement));
+ parentElement.appendChild(parentListItem);
+ this.buildNestedDropdown(item, nestedDropdown);
+ index = index + 1;
+ } else {
+ const listItem = document.createElement('li');
+ listItem.classList.add('item');
+ listItem.setAttribute('role', 'option');
+ listItem.textContent = key;
+ listItem.addEventListener('mouseover', this.onMouseOver.bind(this, index, parentElement));
+ listItem.addEventListener('mousedown', this.onMouseDown.bind(this, key));
+ parentElement.appendChild(listItem);
+ index = index + 1;
+ }
+ });
+ }
+
+ addListItem(item, index, parentElement) {
+ const listItem = document.createElement('li');
+ listItem.setAttribute('role', 'option');
+ listItem.textContent = item;
+ listItem.addEventListener('mouseover', this.onMouseOver.bind(this, index));
+ listItem.addEventListener('mousedown', this.onMouseDown.bind(this, item));
+ parentElement.appendChild(listItem);
+ }
+
+ addEventListeners() {
+ document.addEventListener('keydown', this.onKeyDownBound);
+ this.dropdown.addEventListener('wheel', this.onWheelBound);
+ document.addEventListener('click', this.onClickBound);
+ }
+
+ removeEventListeners() {
+ document.removeEventListener('keydown', this.onKeyDownBound);
+ this.dropdown.removeEventListener('wheel', this.onWheelBound);
+ document.removeEventListener('click', this.onClickBound);
+ }
+
+ onMouseOver(index, parentElement) {
+ if (parentElement) {
+ this.focusedDropdown = parentElement;
+ }
+ this.selectedIndex = index;
+ this.updateSelection();
+ }
+
+ onMouseOut() {
+ this.selectedIndex = -1;
+ this.updateSelection();
+ }
+
+ onMouseDown(suggestion, event) {
+ event.preventDefault();
+ this.onSelect(suggestion);
+ this.dropdown.remove();
+ this.removeEventListeners();
+ }
+
+ onKeyDown(event) {
+ const enterKeyCode = 13;
+ const escKeyCode = 27;
+ const arrowUpKeyCode = 38;
+ const arrowDownKeyCode = 40;
+ const arrowRightKeyCode = 39;
+ const arrowLeftKeyCode = 37;
+ const tabKeyCode = 9;
+
+ const items = Array.from(this.focusedDropdown.children);
+ const selectedItem = items[this.selectedIndex];
+
+ if (activeDropdown) {
+ if (event.keyCode === arrowUpKeyCode) {
+ event.preventDefault();
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
+ this.updateSelection();
+ }
+
+ else if (event.keyCode === arrowDownKeyCode) {
+ event.preventDefault();
+ this.selectedIndex = Math.min(items.length - 1, this.selectedIndex + 1);
+ this.updateSelection();
+ }
+
+ else if (event.keyCode === arrowRightKeyCode) {
+ event.preventDefault();
+ if (selectedItem && selectedItem.classList.contains('folder')) {
+ const nestedDropdown = selectedItem.querySelector('.ttN-nested-dropdown');
+ if (nestedDropdown) {
+ this.focusedDropdown = nestedDropdown;
+ this.selectedIndex = 0;
+ this.updateSelection();
+ }
+ }
+ }
+
+ else if (event.keyCode === arrowLeftKeyCode && this.focusedDropdown !== this.dropdown) {
+ const parentDropdown = this.focusedDropdown.closest('.ttN-dropdown, .ttN-nested-dropdown').parentNode.closest('.ttN-dropdown, .ttN-nested-dropdown');
+ if (parentDropdown) {
+ this.focusedDropdown = parentDropdown;
+ this.selectedIndex = Array.from(parentDropdown.children).indexOf(this.focusedDropdown.parentNode);
+ this.updateSelection();
+ }
+ }
+
+ else if ((event.keyCode === enterKeyCode || event.keyCode === tabKeyCode) && this.selectedIndex >= 0) {
+ event.preventDefault();
+ if (selectedItem.classList.contains('item')) {
+ this.onSelect(items[this.selectedIndex].textContent);
+ this.dropdown.remove();
+ this.removeEventListeners();
+ }
+
+ const nestedDropdown = selectedItem.querySelector('.ttN-nested-dropdown');
+ if (nestedDropdown) {
+ this.focusedDropdown = nestedDropdown;
+ this.selectedIndex = 0;
+ this.updateSelection();
+ }
+ }
+
+ else if (event.keyCode === escKeyCode) {
+ this.dropdown.remove();
+ this.removeEventListeners();
+ }
+ }
+ }
+
+ onWheel(event) {
+ const top = parseInt(this.dropdown.style.top);
+ if (localStorage.getItem("Comfy.Settings.Comfy.InvertMenuScrolling")) {
+ this.dropdown.style.top = (top + (event.deltaY < 0 ? 10 : -10)) + "px";
+ } else {
+ this.dropdown.style.top = (top + (event.deltaY < 0 ? -10 : 10)) + "px";
+ }
+ }
+
+ onClick(event) {
+ if (!this.dropdown.contains(event.target) && event.target !== this.inputEl) {
+ this.dropdown.remove();
+ this.removeEventListeners();
+ }
+ }
+
+ updateSelection() {
+ Array.from(this.focusedDropdown.children).forEach((li, index) => {
+ if (index === this.selectedIndex) {
+ li.classList.add('selected');
+ } else {
+ li.classList.remove('selected');
+ }
+ });
+ }
+}
+
+export function ttN_CreateDropdown(inputEl, suggestions, onSelect, isDict = false) {
+ ttN_RemoveDropdown();
+ new Dropdown(inputEl, suggestions, onSelect, isDict);
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNdynamicWidgets.js b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNdynamicWidgets.js
new file mode 100644
index 0000000000000000000000000000000000000000..08412f5599b8b59499030f906090be926c081785
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNdynamicWidgets.js
@@ -0,0 +1,304 @@
+import { app } from "../../scripts/app.js";
+
+let origProps = {};
+
+const findWidgetByName = (node, name) => node.widgets.find((w) => w.name === name);
+
+const doesInputWithNameExist = (node, name) => node.inputs ? node.inputs.some((input) => input.name === name) : false;
+
+function updateNodeHeight(node) {
+ node.setSize([node.size[0], node.computeSize()[1]]);
+}
+
+function toggleWidget(node, widget, show = false, suffix = "") {
+ if (!widget || doesInputWithNameExist(node, widget.name)) return;
+ if (!origProps[widget.name]) {
+ origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize };
+ }
+ const origSize = node.size;
+
+ widget.type = show ? origProps[widget.name].origType : "ttNhidden" + suffix;
+ widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4];
+
+ widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show));
+
+ const height = show ? Math.max(node.computeSize()[1], origSize[1]) : node.size[1];
+ node.setSize([node.size[0], height]);
+
+}
+
+function widgetLogic(node, widget) {
+ if (widget.name === 'lora_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'lora_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'lora1_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'lora1_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora1_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora1_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora1_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'lora2_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'lora2_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora2_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora2_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora2_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'lora3_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'lora3_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora3_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora3_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora3_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'refiner_ckpt_name') {
+ let refiner_lora1 = findWidgetByName(node, 'refiner_lora1_name').value
+ let refiner_lora2 = findWidgetByName(node, 'refiner_lora2_name').value
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'refiner_vae_name'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_name'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_clip_strength'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_name'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'refiner_vae_name'), true)
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_name'), true)
+ if (refiner_lora1 !== "None") {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_clip_strength'), true)
+ }
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_name'), true)
+ if (refiner_lora2 !== "None") {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_clip_strength'), true)
+ }
+ }
+ }
+ if (widget.name === 'refiner_lora1_name') {
+ let refiner_ckpt = findWidgetByName(node, 'refiner_ckpt_name').value
+
+ if (widget.value === "None" || refiner_ckpt === "None") {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'refiner_lora2_name') {
+ let refiner_ckpt = findWidgetByName(node, 'refiner_ckpt_name').value
+
+ if (widget.value === "None" || refiner_ckpt === "None") {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'rescale_after_model') {
+ if (widget.value === false) {
+ toggleWidget(node, findWidgetByName(node, 'rescale_method'))
+ toggleWidget(node, findWidgetByName(node, 'rescale'))
+ toggleWidget(node, findWidgetByName(node, 'percent'))
+ toggleWidget(node, findWidgetByName(node, 'width'))
+ toggleWidget(node, findWidgetByName(node, 'height'))
+ toggleWidget(node, findWidgetByName(node, 'longer_side'))
+ toggleWidget(node, findWidgetByName(node, 'crop'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'rescale_method'), true)
+ toggleWidget(node, findWidgetByName(node, 'rescale'), true)
+
+ let rescale_value = findWidgetByName(node, 'rescale').value
+
+ if (rescale_value === 'by percentage') {
+ toggleWidget(node, findWidgetByName(node, 'percent'), true)
+ } else if (rescale_value === 'to Width/Height') {
+ toggleWidget(node, findWidgetByName(node, 'width'), true)
+ toggleWidget(node, findWidgetByName(node, 'height'), true)
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'longer_side'), true)
+ }
+ toggleWidget(node, findWidgetByName(node, 'crop'), true)
+ }
+ }
+ if (widget.name === 'rescale') {
+ let rescale_after_model = findWidgetByName(node, 'rescale_after_model').value
+ if (widget.value === 'by percentage' && rescale_after_model) {
+ toggleWidget(node, findWidgetByName(node, 'width'))
+ toggleWidget(node, findWidgetByName(node, 'height'))
+ toggleWidget(node, findWidgetByName(node, 'longer_side'))
+ toggleWidget(node, findWidgetByName(node, 'percent'), true)
+ } else if (widget.value === 'to Width/Height' && rescale_after_model) {
+ toggleWidget(node, findWidgetByName(node, 'width'), true)
+ toggleWidget(node, findWidgetByName(node, 'height'), true)
+ toggleWidget(node, findWidgetByName(node, 'percent'))
+ toggleWidget(node, findWidgetByName(node, 'longer_side'))
+ } else if (rescale_after_model) {
+ toggleWidget(node, findWidgetByName(node, 'longer_side'), true)
+ toggleWidget(node, findWidgetByName(node, 'width'))
+ toggleWidget(node, findWidgetByName(node, 'height'))
+ toggleWidget(node, findWidgetByName(node, 'percent'))
+ }
+ }
+ if (widget.name === 'upscale_method') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'factor'))
+ toggleWidget(node, findWidgetByName(node, 'crop'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'factor'), true)
+ toggleWidget(node, findWidgetByName(node, 'crop'), true)
+ }
+ }
+ if (widget.name === 'image_output') {
+ if (widget.value === 'Hide' || widget.value === 'Preview') {
+ toggleWidget(node, findWidgetByName(node, 'save_prefix'))
+ toggleWidget(node, findWidgetByName(node, 'output_path'))
+ toggleWidget(node, findWidgetByName(node, 'embed_workflow'))
+ toggleWidget(node, findWidgetByName(node, 'number_padding'))
+ toggleWidget(node, findWidgetByName(node, 'overwrite_existing'))
+ } else if (widget.value === 'Save' || widget.value === 'Hide/Save') {
+ toggleWidget(node, findWidgetByName(node, 'save_prefix'), true)
+ toggleWidget(node, findWidgetByName(node, 'output_path'), true)
+ toggleWidget(node, findWidgetByName(node, 'embed_workflow'), true)
+ toggleWidget(node, findWidgetByName(node, 'number_padding'), true)
+ toggleWidget(node, findWidgetByName(node, 'overwrite_existing'), true)
+ }
+ }
+ if (widget.name === 'add_noise') {
+ if (widget.value === "disable") {
+ toggleWidget(node, findWidgetByName(node, 'noise_seed'))
+ toggleWidget(node, findWidgetByName(node, 'control_after_generate'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'noise_seed'), true)
+ toggleWidget(node, findWidgetByName(node, 'control_after_generate'), true)
+ }
+ }
+ if (widget.name === 'ckpt_B_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'config_B_name'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'config_B_name'), true)
+ }
+ }
+ if (widget.name === 'ckpt_C_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'config_C_name'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'config_C_name'), true)
+ }
+ }
+ if (widget.name === 'save_model') {
+ if (widget.value === "True") {
+ toggleWidget(node, findWidgetByName(node, 'save_prefix'), true)
+
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'save_prefix'))
+ }
+ }
+ if (widget.name === 'num_loras') {
+ let number_to_show = widget.value + 1
+ for (let i = 0; i < number_to_show; i++) {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_name'), true)
+ if (findWidgetByName(node, 'mode').value === "simple") {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'), true)
+ }
+ }
+ for (let i = number_to_show; i < 21; i++) {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_name'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'))
+ }
+ updateNodeHeight(node)
+ }
+ if (widget.name === 'mode') {
+ let number_to_show = findWidgetByName(node, 'num_loras').value + 1
+ for (let i = 0; i < number_to_show; i++) {
+ if (widget.value === "simple") {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'), true)}
+ }
+ updateNodeHeight(node)
+ }
+ if (widget.name === 'toggle') {
+ widget.type = 'toggle'
+ widget.options = {on: 'Enabled', off: 'Disabled'}
+ }
+}
+
+const getSetWidgets = ['rescale_after_model', 'rescale', 'image_output',
+ 'lora_name', 'lora1_name', 'lora2_name', 'lora3_name',
+ 'refiner_lora1_name', 'refiner_lora2_name', 'upscale_method',
+ 'image_output', 'add_noise',
+ 'ckpt_B_name', 'ckpt_C_name', 'save_model', 'refiner_ckpt_name',
+ 'num_loras', 'mode', 'toggle']
+
+function getSetters(node) {
+ if (node.widgets)
+ for (const w of node.widgets) {
+ if (getSetWidgets.includes(w.name)) {
+ widgetLogic(node, w);
+ let widgetValue = w.value;
+
+ // Define getters and setters for widget values
+ Object.defineProperty(w, 'value', {
+ get() {
+ return widgetValue;
+ },
+ set(newVal) {
+ if (newVal !== widgetValue) {
+ widgetValue = newVal;
+ widgetLogic(node, w);
+ }
+ }
+ });
+ }
+ }
+}
+
+app.registerExtension({
+ name: "comfy.ttN.dynamicWidgets",
+
+ nodeCreated(node) {
+ if (node.getTitle() == "hiresfixScale" ||
+ node.getTitle() == "pipeLoader" ||
+ node.getTitle() == "pipeLoaderSDXL" ||
+ node.getTitle() == "pipeKSampler" ||
+ node.getTitle() == "pipeKSamplerAdvanced" ||
+ node.getTitle() == "pipeKSamplerSDXL" ||
+ node.getTitle() == "imageRemBG" ||
+ node.getTitle() == "imageOutput"||
+ node.getTitle() == "multiModelMerge" ||
+ node.getTitle() == "pipeLoraStack" ||
+ node.getTitle() == "pipeEncodeConcat") {
+ getSetters(node)
+ }
+ }
+});
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNembedAC.js b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNembedAC.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e673c0c4e98e0e6b84f7b58dca13729b3892d31
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNembedAC.js
@@ -0,0 +1,170 @@
+// Imports specific objects from other modules.
+import { app } from "../../scripts/app.js";
+import { ttN_CreateDropdown, ttN_RemoveDropdown } from "./ttN.js";
+
+// Initialize some global lists and objects.
+let embeddingsList = [];
+let embeddingFiles = [];
+let embeddingsHierarchy = {};
+
+// Convert a list of strings into a hierarchical structure.
+function convertListToHierarchy(list) {
+ const hierarchy = {}; // Initialize an empty hierarchy object.
+
+ // Iterate over each item in the list.
+ for (var item of list) {
+ item = item.replace("embedding:", ""); // Remove any "embedding:" prefix from the item.
+ const parts = item.split(/:\\|\\/); // Split the item by either ':\' or '\'.
+ let currentNode = hierarchy; // Start at the root of the hierarchy.
+
+ // For each part of the split item...
+ parts.forEach((part, index) => {
+ // If it's the last part, set its value to null in the hierarchy.
+ if (index === parts.length - 1) {
+ currentNode[part] = null;
+ } else {
+ // Otherwise, initialize the node if it doesn't exist yet and move deeper into the hierarchy.
+ currentNode[part] = currentNode[part] || {};
+ currentNode = currentNode[part];
+ }
+ });
+ }
+
+ return hierarchy; // Return the filled hierarchy.
+}
+
+// Register an extension to the app.
+app.registerExtension({
+ name: "comfy.ttN.embeddingAC",
+ // Before a node definition is registered...
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ // If the node name matches a specific type...
+ if (nodeData.name === "ttN pipeKSampler") {
+ initializeEmbeddingData(nodeData.input.hidden.embeddingsList[0]);
+ }
+ },
+ // When a node is created...
+ nodeCreated(node) {
+ // If the node has widgets and its title isn't "xyPlot"...
+ if (node.widgets && node.getTitle() !== "xyPlot") {
+ const relevantWidgets = filterRelevantWidgets(node.widgets); // Filter out the relevant widgets.
+ addInputListenersToWidgets(relevantWidgets); // Add input listeners to these widgets.
+ }
+ }
+});
+
+// Returns a list of widgets that either have type "customtext" with dynamic prompts or just have dynamic prompts.
+function filterRelevantWidgets(widgets) {
+ return widgets.filter(widget => (widget.type === "customtext" && widget.dynamicPrompts !== false) || widget.dynamicPrompts);
+}
+
+// Adds input listeners to the given widgets.
+function addInputListenersToWidgets(widgets) {
+ widgets.forEach(widget => {
+ const inputHandler = createWidgetInputHandler(widget); // Create an input handler specific for this widget.
+ setWidgetInputHandler(widget, inputHandler); // Set this handler to the widget.
+ });
+}
+
+// Returns a function that will handle the widget's input.
+function createWidgetInputHandler(widget) {
+ return function handleInput() {
+ const currentWord = getCurrentWordFromInput(widget); // Get the word at the current cursor position in the widget's input.
+ // Check if the current word should trigger embedding suggestions...
+ if (shouldProvideEmbeddingSuggestion(currentWord)) {
+ const suggestions = filterEmbeddingsForInput(currentWord); // Get suggestions for the current word.
+ if (suggestions.length > 0) { // If there are suggestions...
+ // Convert the suggestions to a hierarchy and create a dropdown with these suggestions.
+ embeddingsHierarchy = convertListToHierarchy(suggestions);
+ ttN_CreateDropdown(widget.inputEl, embeddingsHierarchy, selectedSuggestion => {
+ // Update the widget's input value with the selected suggestion when one is chosen.
+ widget.inputEl.value = updateInputWithSuggestion(widget.inputEl.value, selectedSuggestion, widget);
+ }, true);
+ return;
+ }
+ }
+ // If no suggestions, remove any existing dropdown.
+ ttN_RemoveDropdown();
+ };
+}
+
+// Adds or replaces event listeners for the widget's input.
+function setWidgetInputHandler(widget, handler) {
+ ['input', 'mousedown'].forEach(event => {
+ // Remove any existing listeners and then add the new handler.
+ widget.inputEl.removeEventListener(event, handler);
+ widget.inputEl.addEventListener(event, handler);
+ });
+}
+
+// Returns the word at the current cursor position from the widget's input.
+function getCurrentWordFromInput(widget) {
+ const cursorPosition = widget.inputEl.selectionStart;
+ const segments = widget.inputEl.value.split(' ');
+ return segments[widget.inputEl.value.substring(0, cursorPosition).split(' ').length - 1].toLowerCase();
+}
+
+// Determines if the current word should trigger embedding suggestions.
+function shouldProvideEmbeddingSuggestion(word) {
+ const suggestionPrefix = 'embedding:';
+ return suggestionPrefix.startsWith(word) && word.length > 2 || word.startsWith(suggestionPrefix);
+}
+
+// Filters embeddings based on a specific word.
+function filterEmbeddingsForInput(input) {
+ const prefixes = ['embedding', 'embeddin', 'embeddi', 'embedd', 'embed', 'embe', 'emb']
+
+ let inputLowered = input.toLowerCase();
+ let cleanedInput = inputLowered.replace('embedding:', '');
+
+ prefixes.forEach(prefix => {
+ if (inputLowered.startsWith(prefix)) {
+ cleanedInput = cleanedInput.replace(prefix, '');
+ }
+ })
+
+ cleanedInput = cleanedInput.replace(/\//g, "\\");
+
+ return embeddingsList.filter(embedding => {
+ const embeddingName = getFileName(embedding).toLowerCase();
+ embedding = embedding.replace('embedding:', '').toLowerCase();
+ if (embeddingName.startsWith(cleanedInput) || embedding.startsWith(cleanedInput) || prefixes.includes(cleanedInput)) {
+ return true;
+ }
+ return false
+ });
+}
+
+function getFileName(path) {
+ const parts = path.split(/[\/:\\]/); // Split the path by '/' or ':'
+ const fileName = parts[parts.length - 1]; // Get the last part (filename with extension)
+ return fileName;
+}
+
+// Updates the widget's input text with a selected suggestion.
+function updateInputWithSuggestion(inputText, selectedSuggestion, widget) {
+ const cursorPosition = widget.inputEl.selectionStart;
+ const inputSegments = inputText.split(' ');
+ const cursorSegmentIndex = inputText.substring(0, cursorPosition).split(' ').length - 1;
+
+ if (inputSegments[cursorSegmentIndex].startsWith('emb')) {
+ inputSegments[cursorSegmentIndex] = 'embedding:' + selectedSuggestion;
+ }
+
+ return inputSegments.join(' ');
+}
+
+// Initializes data related to embeddings.
+function initializeEmbeddingData(initialEmbeddingsList) {
+ embeddingsList = initialEmbeddingsList;
+
+ embeddingsList.forEach(embedding => {
+ const fileName = embedding.split('\\').slice(-1)[0];
+ embeddingFiles.push(fileName);
+ });
+
+ embeddingsList = embeddingsList.map(embedding => {
+ const segments = embedding.split('/');
+ return segments.map((segment, index) => "embedding:" + segments.slice(0, index + 1).join('/'));
+ }).flat();
+}
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNfullscreen.js b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNfullscreen.js
new file mode 100644
index 0000000000000000000000000000000000000000..1333e605b222e2eac91a448eb2074ffa3714c46d
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNfullscreen.js
@@ -0,0 +1,540 @@
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js";
+
+const FULLSCREEN_WRAPPER_ID = "ttN-FullscreenWrapper";
+const FULLSCREEN_IMAGE_ID = "ttN-FullscreenImage";
+const IMAGE_PREVIEWS_WRAPPER_ID = "ttN-imagePreviewsWrapper";
+
+let ttN_isFullscreen = false;
+let ttN_FullscreenImage = new Image();
+let ttN_FullscreenImageIndex = 0;
+let ttN_FullscreenNode = null;
+let ttN_Slideshow = true;
+
+let ttN_srcDict = {};
+let ttN_imageElementsDict = {};
+
+loadSrcDict()
+
+const ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
+
+function saveSrcDict() {
+ sessionStorage.setItem('ttN_srcDict', JSON.stringify(ttN_srcDict));
+}
+
+function loadSrcDict() {
+ const savedData = sessionStorage.getItem('ttN_srcDict');
+ if (savedData) {
+ ttN_srcDict = JSON.parse(savedData);
+ }
+}
+
+function clearSrcDict() {
+ ttN_srcDict = {};
+ sessionStorage.removeItem('ttN_srcDict');
+}
+
+function _getSelectedNode() {
+ const graphcanvas = LGraphCanvas.active_canvas;
+ if (graphcanvas.selected_nodes && Object.keys(graphcanvas.selected_nodes).length === 1) {
+ return Object.values(graphcanvas.selected_nodes)[0];
+ }
+ return null;
+}
+
+function _findFullImageSRC(node) {
+ if (node.imgs) {
+ let img = node.imgs.find(imgElement => imgElement.src.includes("filename"));
+ return img ? img.src : null;
+ }
+ return null;
+}
+
+function _findLatentPreviewImageSRC(node) {
+ if (!node.imgs) return null;
+
+ if (node.imageIndex !== null && node.imageIndex < node.imgs.length) {
+ return node.imgs[node.imageIndex].src;
+ } else if (node.overIndex !== null && node.overIndex < node.imgs.length) {
+ return node.imgs[node.overIndex].src;
+ }
+ return null;
+}
+
+function _initiateFullscreen(Element) {
+ if (Element.requestFullscreen) {
+ return Element.requestFullscreen();
+ } else if (Element.mozRequestFullScreen) {
+ return Element.mozRequestFullScreen();
+ } else if (Element.webkitRequestFullscreen) {
+ return Element.webkitRequestFullscreen();
+ } else if (Element.msRequestFullscreen) {
+ return Element.msRequestFullscreen();
+ }
+}
+
+function _applyTranslation(Element, Index, List) {
+ let translationConst = (Index / (List.length - 1)) * 100;
+
+ translationConst = translationConst - (0.5 / (List.length - 1)) * 100
+
+ if (Index === 0) { translationConst = 0; }
+ Element.style.transform = 'translateX(-' + translationConst + '%)';
+}
+
+function _handleArrowKeys(e) {
+ e.stopPropagation();
+
+ const FullscreenWrapper = document.getElementById(FULLSCREEN_WRAPPER_ID);
+ const imagePreviewsWrapper = document.getElementById(IMAGE_PREVIEWS_WRAPPER_ID);
+ const imageList = ttN_srcDict[ttN_FullscreenNode.id] || [];
+ const comfyMenu = document.getElementsByClassName("comfy-menu")[0]
+ const litegraph = document.getElementsByClassName("litegraph")[0]
+
+
+ switch (e.code) {
+ case 'ArrowLeft':
+ if (e.ctrlKey) {
+ ttN_FullscreenImageIndex = 0;
+ break;
+ }
+
+ if (e.shiftKey) {
+ ttN_FullscreenImageIndex -= 5;
+ if (ttN_FullscreenImageIndex < 0) ttN_FullscreenImageIndex = 0
+ break;
+ }
+
+ ttN_FullscreenImageIndex -= 1;
+ if (ttN_FullscreenImageIndex < 0) ttN_FullscreenImageIndex = 0
+ break;
+
+ case 'ArrowRight':
+ if (e.ctrlKey) {
+ ttN_FullscreenImageIndex = imageList.length - 1;
+ break;
+ }
+
+ if (e.shiftKey) {
+ ttN_FullscreenImageIndex += 5;
+ if (ttN_FullscreenImageIndex > imageList.length - 1) ttN_FullscreenImageIndex = imageList.length - 1
+ break;
+ }
+
+ ttN_FullscreenImageIndex += 1;
+ if (ttN_FullscreenImageIndex > imageList.length - 1) ttN_FullscreenImageIndex = imageList.length - 1
+ break;
+
+ case 'ArrowUp':
+ if (imagePreviewsWrapper) {
+ if (imagePreviewsWrapper.style.display === 'none') {
+ FullscreenWrapper.append(comfyMenu)
+ imagePreviewsWrapper.style.display = 'flex'
+ } else {
+ litegraph.append(comfyMenu)
+ imagePreviewsWrapper.style.display = 'none'
+ }
+ }
+ break;
+
+ case 'ArrowDown':
+ ttN_Slideshow = !ttN_Slideshow;
+
+ if (ttN_Slideshow) {
+ ttN_FullscreenImageIndex = -1;
+ imagePreviewsWrapper.style.display = 'none'
+ litegraph.append(comfyMenu)
+ } else {
+ imagePreviewsWrapper.style.display = 'flex'
+ FullscreenWrapper.append(comfyMenu)
+ }
+ break;
+ }
+ _applyTranslation(imagePreviewsWrapper, ttN_FullscreenImageIndex, imageList);
+ updateImageElements()
+}
+
+function _handleWheelEvent(e) {
+ e.stopPropagation();
+
+ var InvertMenuScrolling = localStorage.getItem('Comfy.Settings.Comfy.InvertMenuScrolling')
+ console.log("InvertMenuScrolling", InvertMenuScrolling)
+
+ if (InvertMenuScrolling === "true") {
+ console.log('yes')
+ if (e.deltaY > 0) {
+ // Scrolling down
+ ttN_FullscreenImageIndex += 1;
+ } else if (e.deltaY < 0) {
+ // Scrolling up
+ ttN_FullscreenImageIndex -= 1;
+ if (ttN_FullscreenImageIndex < 0) ttN_FullscreenImageIndex = 0;
+ }
+ } else {
+ console.log('no')
+ if (e.deltaY < 0) {
+ // Scrolling down
+ ttN_FullscreenImageIndex += 1;
+ } else if (e.deltaY > 0) {
+ // Scrolling up
+ ttN_FullscreenImageIndex -= 1;
+ if (ttN_FullscreenImageIndex < 0) ttN_FullscreenImageIndex = 0;
+ }
+ }
+ updateImageElements()
+}
+
+function _handleEscapeKey(e) {
+ e.stopPropagation();
+ LGraphCanvas.prototype.ttNcloseFullscreen();
+}
+
+function _handleExecutedEvent(event) {
+ setTimeout(updateImageTLDE, 500);
+}
+
+function _handleReconnectingEvent(e) {
+ clearSrcDict();
+ sessionStorage.removeItem('Comfy.Settings.ttN.default_fullscreen_node');
+ ttN_imageElementsDict = {};
+}
+
+function _triggerFullscreen(node) {
+ updateImageTLDE();
+ ttN_FullscreenImageIndex = -1;
+ LGraphCanvas.prototype.ttNcreateFullscreen(node);
+ ttN_FullscreenNode = node;
+}
+
+function ttNfullscreenEventListener(e) {
+ if (!ttN_isFullscreen) {
+ if ((e.code === 'ArrowUp' && e.shiftKey) && !e.ctrlKey) {
+ e.stopPropagation();
+
+ let selected_node = _getSelectedNode();
+ if (selected_node) {
+ _triggerFullscreen(selected_node);
+ return
+ }
+
+ let defaultNodeID = JSON.parse(sessionStorage.getItem('Comfy.Settings.ttN.default_fullscreen_node'));
+ if (defaultNodeID) {
+ let defaultNode = app.graph._nodes_by_id[defaultNodeID];
+ if (defaultNode) {
+ _triggerFullscreen(defaultNode);
+ return
+ }
+ }
+ }
+
+ return;
+ }
+
+ e.stopPropagation();
+
+ updateImageTLDE();
+
+ const imagePreviewsWrapper = document.getElementById(IMAGE_PREVIEWS_WRAPPER_ID);
+ const imageList = ttN_srcDict[ttN_FullscreenNode.id] || [];
+
+ switch (e.type) {
+ case 'wheel':
+ _handleWheelEvent(e);
+ _applyTranslation(imagePreviewsWrapper, ttN_FullscreenImageIndex, imageList);
+ break;
+ case 'keydown':
+ if (ARROW_KEYS.includes(e.code)) {
+ _handleArrowKeys(e);
+ _applyTranslation(imagePreviewsWrapper, ttN_FullscreenImageIndex, imageList);
+ } else if (e.code === 'Escape') {
+ _handleEscapeKey(e);
+ _applyTranslation(imagePreviewsWrapper, ttN_FullscreenImageIndex, imageList);
+ }
+ break;
+ }
+}
+
+function updateImageTLDE() {
+ for (let node of app.graph._nodes) {
+ if (!node.imgs) continue
+
+ let imgSrc = _findFullImageSRC(node);
+ if (!imgSrc) continue;
+
+ _removeLatentPreviewImageSRC(node, imgSrc)
+
+ ttN_srcDict[node.id] = ttN_srcDict[node.id] || [];
+
+ let index = ttN_srcDict[node.id].length;
+
+ if (!ttN_srcDict[node.id].includes((index, imgSrc))) {
+ ttN_srcDict[node.id].push((index, imgSrc));
+ if (ttN_Slideshow) {
+ updateImageElements(index);
+ }
+ }
+
+ }
+ saveSrcDict();
+ updateImageElements();
+};
+
+function _getImageDivFromSrc(imgSrc, index) {
+ // If image element doesn't exist, create it
+ if (!ttN_imageElementsDict[imgSrc]) {
+ const imgWrapper = document.createElement('div');
+ imgWrapper.classList.add('ttN-imgWrapper');
+
+ const imgElement = document.createElement('img');
+ imgElement.src = imgSrc;
+ imgElement.classList.add('ttN-img');
+ imgWrapper.appendChild(imgElement);
+
+ imgElement.addEventListener('click', () => {
+ ttN_FullscreenImageIndex = index;
+ updateImageElements(index);
+ });
+
+ ttN_imageElementsDict[imgSrc] = imgWrapper;
+ }
+ return ttN_imageElementsDict[imgSrc];
+}
+
+function _removeLatentPreviewImageSRC(node, imgSrc) {
+ let latentPreviewSrc = _findLatentPreviewImageSRC(node);
+ if (imgSrc && latentPreviewSrc) {
+ for (let i in node.imgs) {
+ if(!node.imgs[i].src.includes("filename")) {
+ node.imgs.splice(i, 1);
+ }
+ }
+ }
+}
+
+function _handleLatentPreview(imgDivList) {
+ let latentPreview = _findLatentPreviewImageSRC(ttN_FullscreenNode);
+ if (latentPreview && !latentPreview.includes("filename") && ttN_FullscreenImageIndex === imgDivList.length - 1) {
+ ttN_FullscreenImage.src = latentPreview
+ }
+}
+
+function updateImageElements(indexOverride = null) {
+ if (!ttN_isFullscreen) return;
+
+ const srcList = ttN_srcDict[ttN_FullscreenNode.id] || null;
+ if (!srcList) return;
+
+ const fullscreenWrapper = document.getElementById(FULLSCREEN_WRAPPER_ID);
+ if (!fullscreenWrapper) return
+
+ const imgDivList = srcList.map((src, index) => _getImageDivFromSrc(src, index));
+
+ ttN_FullscreenImageIndex = indexOverride || ttN_FullscreenImageIndex
+ if ((ttN_FullscreenImageIndex > imgDivList.length - 1) || (ttN_FullscreenImageIndex === -1)) {
+ ttN_FullscreenImageIndex = imgDivList.length - 1
+ }
+ if (ttN_FullscreenImageIndex < -1) ttN_FullscreenImageIndex = 0
+
+ ttN_FullscreenImage.src = imgDivList[ttN_FullscreenImageIndex].children[0].src;
+
+ const previewsWrapper = document.getElementById(IMAGE_PREVIEWS_WRAPPER_ID);
+ if (!previewsWrapper) return
+
+ if (ttN_Slideshow) {
+ fullscreenWrapper.classList.add('ttN-slideshow')
+ _handleLatentPreview(imgDivList);
+ } else {
+ fullscreenWrapper.classList.remove('ttN-slideshow')
+ }
+
+ if (ttN_FullscreenImageIndex > imgDivList.length - 1) ttN_FullscreenImageIndex = imgDivList.length - 1;
+ if (ttN_FullscreenImageIndex < 0) ttN_FullscreenImageIndex = 0;
+
+ imgDivList.forEach((imgDiv, index) => {
+ if (previewsWrapper.children[index] != imgDiv) previewsWrapper.appendChild(imgDiv);
+
+ const orderValue = index - ttN_FullscreenImageIndex;
+ imgDiv.style.order = orderValue;
+
+ if (index < ttN_FullscreenImageIndex) {
+ // For images before the selected image
+ imgDiv.classList.remove('ttN-divSelected', 'ttN-divAfter');
+ imgDiv.children[0].classList.remove('ttN-imgSelected', 'ttN-imgAfter');
+
+ imgDiv.classList.add('ttN-divBefore');
+ //imgDiv.children[0].classList.add('ttN-imgBefore');
+ }
+ else if (index === ttN_FullscreenImageIndex) {
+ // For the selected image
+ imgDiv.classList.remove('ttN-divBefore', 'ttN-divAfter');
+ imgDiv.children[0].classList.remove('ttN-imgBefore', 'ttN-imgAfter');
+
+ imgDiv.classList.add('ttN-divSelected');
+ imgDiv.children[0].classList.add('ttN-imgSelected');
+ }
+ else if (index > ttN_FullscreenImageIndex) {
+ // For images after the selected image
+ imgDiv.classList.remove('ttN-divSelected', 'ttN-divBefore');
+ imgDiv.children[0].classList.remove('ttN-imgSelected', 'ttN-imgBefore');
+
+ imgDiv.classList.add('ttN-divAfter');
+ //imgDiv.children[0].classList.add('ttN-imgAfter');
+ }
+ });
+
+ _applyTranslation(previewsWrapper, ttN_FullscreenImageIndex, imgDivList);
+}
+
+api.addEventListener("status", _handleExecutedEvent);
+api.addEventListener("progress", _handleExecutedEvent);
+api.addEventListener("execution_cached", _handleExecutedEvent);
+api.addEventListener("reconnecting", _handleReconnectingEvent);
+
+app.registerExtension({
+ name: "comfy.ttN.fullscreen",
+ init() {
+ document.addEventListener("keydown", ttNfullscreenEventListener, true);
+ },
+ setup() {
+ LGraphCanvas.prototype.ttNcreateFullscreen = function (node) {
+ if (!node || ttN_isFullscreen) return;
+
+ document.addEventListener("wheel", ttNfullscreenEventListener, true);
+
+ const fullscreenWrapper = document.createElement('div');
+ fullscreenWrapper.id = FULLSCREEN_WRAPPER_ID;
+ fullscreenWrapper.classList.add('ttN-slideshow');
+ document.body.appendChild(fullscreenWrapper);
+
+ const fullscreenImage = new Image();
+ fullscreenImage.src = _findFullImageSRC(node) || _findLatentPreviewImageSRC(node) || '';
+ fullscreenImage.id = FULLSCREEN_IMAGE_ID;
+ fullscreenWrapper.appendChild(fullscreenImage);
+
+ const previewsWrapper = document.createElement('div');
+ previewsWrapper.id = IMAGE_PREVIEWS_WRAPPER_ID;
+ previewsWrapper.style.display = 'none';
+
+ fullscreenWrapper.appendChild(previewsWrapper);
+
+ _initiateFullscreen(fullscreenWrapper).then(() => {
+ ttN_isFullscreen = true;
+ ttN_FullscreenImage = fullscreenImage;
+ updateImageElements();
+ }).catch(err => {
+ console.error("Error attempting to enable full-screen mode:", err.message, err.name);
+ });
+
+ fullscreenWrapper.onfullscreenchange = function (event) {
+ if (!document.fullscreenElement) {
+ LGraphCanvas.prototype.ttNcloseFullscreen();
+ }
+ };
+ }
+
+ LGraphCanvas.prototype.ttNcloseFullscreen = function () {
+ if (!ttN_isFullscreen) return;
+
+ const comfyMenu = document.getElementsByClassName("comfy-menu")[0]
+ const litegraph = document.getElementsByClassName("litegraph")[0]
+ litegraph.append(comfyMenu);
+
+ const fullscreenWrapper = document.getElementById(FULLSCREEN_WRAPPER_ID);
+ document.body.removeChild(fullscreenWrapper);
+
+ document.removeEventListener("wheel", ttNfullscreenEventListener, true);
+
+ ttN_isFullscreen = false;
+ }
+
+ const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
+ LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
+ const options = getNodeMenuOptions.apply(this, arguments);
+ node.setDirtyCanvas(true, true);
+
+ options.splice(options.length - 1, 0,
+ {
+ content: "Fullscreen (ttN)",
+ callback: () => { LGraphCanvas.prototype.ttNcreateFullscreen(node) }
+ },
+ {
+ content: "Set Default Fullscreen Node (ttN)",
+ callback: function () {
+ let selectedNode = _getSelectedNode();
+ if (selectedNode) {
+ sessionStorage.setItem('Comfy.Settings.ttN.default_fullscreen_node', JSON.stringify(selectedNode.id));
+ }
+ }
+ },
+ {
+ content: "Clear Default Fullscreen Node (ttN)",
+ callback: function () {
+ sessionStorage.removeItem('Comfy.Settings.ttN.default_fullscreen_node');
+ }
+ },
+ null
+ );
+
+ return options;
+ };
+ }
+});
+
+var styleElement = document.createElement("style");
+const cssCode = `
+
+#ttN-FullscreenWrapper {
+ display: flex;
+ justify-content: center;
+ align-items: end;
+ background-color: #1f1f1f;
+}
+
+#ttN-FullscreenImage {
+ height: inherit;
+ position: absolute;
+}
+
+#ttN-imagePreviewsWrapper {
+ position: absolute;
+ width: max-content;
+ z-index: 1;
+ height: 14vh;
+ left: 50vw;
+ transition: transform 0.2s ease;
+}
+
+.ttN-imgWrapper {
+ position: sticky;
+ transition: transform 0.2s ease;
+ align-self: end;
+}
+
+.ttN-img {
+ height: 121px;
+ margin: 7px;
+ cursor: pointer;
+ display: block;
+ border: 3px solid rgba(255,255,255);
+ box-shadow: 0px 0px 0px 10px;
+ transition: all 0.4s ease;
+}
+
+.ttN-img:hover {
+ transform: scale(1.1);
+ z-index: 1;
+}
+
+.ttN-imgSelected {
+ height: 200px!important;
+ z-index: 1;
+ transition: 0.1s;
+}
+.ttN-slideshow {
+ background: black!important;
+}
+
+`;
+
+styleElement.innerHTML = cssCode
+document.head.appendChild(styleElement);
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNinterface.js b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNinterface.js
new file mode 100644
index 0000000000000000000000000000000000000000..d30cf02d2c48a3faee5aa7596fc9dcd9acf8b5dd
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNinterface.js
@@ -0,0 +1,587 @@
+import { app } from "../../scripts/app.js";
+
+const customPipeLineLink = "#7737AA"
+const customPipeLineSDXLLink = "#0DC52B"
+const customIntLink = "#29699C"
+const customXYPlotLink = "#74DA5D"
+
+var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {};
+if (!customLinkColors["PIPE_LINE"] || !LGraphCanvas.link_type_colors["PIPE_LINE"]) {customLinkColors["PIPE_LINE"] = customPipeLineLink;}
+if (!customLinkColors["PIPE_LINE_SDXL"] || !LGraphCanvas.link_type_colors["PIPE_LINE_SDXL"]) {customLinkColors["PIPE_LINE_SDXL"] = customPipeLineSDXLLink;}
+if (!customLinkColors["INT"] || !LGraphCanvas.link_type_colors["INT"]) {customLinkColors["INT"] = customIntLink;}
+if (!customLinkColors["XYPLOT"] || !LGraphCanvas.link_type_colors["XYPLOT"]) {customLinkColors["XYPLOT"] = customXYPlotLink;}
+
+localStorage.setItem('Comfy.Settings.ttN.customLinkColors', JSON.stringify(customLinkColors));
+
+let ttNisFullscreen = false;
+let ttNcurrentFullscreenImage = null;
+let ttNcurrentNode = null;
+
+app.registerExtension({
+ name: "comfy.ttN.interface",
+ init() {
+ function adjustToGrid(val, gridSize) {
+ return Math.round(val / gridSize) * gridSize;
+ }
+
+ function moveNodeBasedOnKey(e, node, gridSize, shiftMult) {
+ switch (e.code) {
+ case 'ArrowUp':
+ node.pos[1] -= gridSize * shiftMult;
+ break;
+ case 'ArrowDown':
+ node.pos[1] += gridSize * shiftMult;
+ break;
+ case 'ArrowLeft':
+ node.pos[0] -= gridSize * shiftMult;
+ break;
+ case 'ArrowRight':
+ node.pos[0] += gridSize * shiftMult;
+ break;
+ }
+ node.setDirtyCanvas(true, true);
+ }
+
+ function keyMoveNode(e, node) {
+ let gridSize = JSON.parse(localStorage.getItem('Comfy.Settings.Comfy.SnapToGrid.GridSize'));
+ gridSize = gridSize ? parseInt(gridSize) : 1;
+ let shiftMult = e.shiftKey ? 10 : 1;
+
+ node.pos[0] = adjustToGrid(node.pos[0], gridSize);
+ node.pos[1] = adjustToGrid(node.pos[1], gridSize);
+
+ moveNodeBasedOnKey(e, node, gridSize, shiftMult);
+ }
+
+ function getSelectedNodes(e) {
+ const inputField = e.composedPath()[0];
+ if (inputField.tagName === "TEXTAREA") return;
+ if (e.ctrlKey && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) {
+ let graphcanvas = LGraphCanvas.active_canvas;
+ for (let node in graphcanvas.selected_nodes) {
+ keyMoveNode(e, graphcanvas.selected_nodes[node]);
+ }
+ }
+ }
+
+ window.addEventListener("keydown", getSelectedNodes, true);
+
+ LGraphCanvas.prototype.ttNcreateDialog = function (htmlContent, onOK, onCancel) {
+ var dialog = document.createElement("div");
+ dialog.is_modified = false;
+ dialog.className = "ttN-dialog";
+ dialog.innerHTML = htmlContent + "";
+
+ dialog.close = function() {
+ if (dialog.parentNode) {
+ dialog.parentNode.removeChild(dialog);
+ }
+ };
+
+ var inputs = Array.from(dialog.querySelectorAll("input, select"));
+
+ inputs.forEach(input => {
+ input.addEventListener("keydown", function(e) {
+ dialog.is_modified = true;
+ if (e.keyCode == 27) { // ESC
+ onCancel && onCancel();
+ dialog.close();
+ } else if (e.keyCode == 13) { // Enter
+ onOK && onOK(dialog, inputs.map(input => input.value));
+ dialog.close();
+ } else if (e.keyCode != 13 && e.target.localName != "textarea") {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ });
+
+ var graphcanvas = LGraphCanvas.active_canvas;
+ var canvas = graphcanvas.canvas;
+
+ var rect = canvas.getBoundingClientRect();
+ var offsetx = -20;
+ var offsety = -20;
+ if (rect) {
+ offsetx -= rect.left;
+ offsety -= rect.top;
+ }
+
+ if (event) {
+ dialog.style.left = event.clientX + offsetx + "px";
+ dialog.style.top = event.clientY + offsety + "px";
+ } else {
+ dialog.style.left = canvas.width * 0.5 + offsetx + "px";
+ dialog.style.top = canvas.height * 0.5 + offsety + "px";
+ }
+
+ var button = dialog.querySelector("#ok");
+ button.addEventListener("click", function() {
+ onOK && onOK(dialog, inputs.map(input => input.value));
+ dialog.close();
+ });
+
+ canvas.parentNode.appendChild(dialog);
+
+ if(inputs) inputs[0].focus();
+
+ var dialogCloseTimer = null;
+ dialog.addEventListener("mouseleave", function(e) {
+ if(LiteGraph.dialog_close_on_mouse_leave)
+ if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave)
+ dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close();
+ });
+ dialog.addEventListener("mouseenter", function(e) {
+ if(LiteGraph.dialog_close_on_mouse_leave)
+ if(dialogCloseTimer) clearTimeout(dialogCloseTimer);
+ });
+
+ return dialog;
+ };
+
+ LGraphCanvas.prototype.ttNsetNodeDimension = function (node) {
+ const nodeWidth = node.size[0];
+ const nodeHeight = node.size[1];
+
+ let input_html = "";
+ input_html += "";
+
+ LGraphCanvas.prototype.ttNcreateDialog("Width/Height" + input_html,
+ function(dialog, values) {
+ var widthValue = Number(values[0]) ? values[0] : nodeWidth;
+ var heightValue = Number(values[1]) ? values[1] : nodeHeight;
+ let sz = node.computeSize();
+ node.setSize([Math.max(sz[0], widthValue), Math.max(sz[1], heightValue)]);
+ if (dialog.parentNode) {
+ dialog.parentNode.removeChild(dialog);
+ }
+ node.setDirtyCanvas(true, true);
+ },
+ null
+ );
+ };
+
+ LGraphCanvas.prototype.ttNsetSlotTypeColor = function(slot){
+ var slotColor = LGraphCanvas.link_type_colors[slot.output.type].toUpperCase();
+ var slotType = slot.output.type;
+ // Check if the color is in the correct format
+ if (!/^#([0-9A-F]{3}){1,2}$/i.test(slotColor)) {
+ slotColor = "#FFFFFF";
+ }
+
+ // Check if browser supports color input type
+ var inputType = "color";
+ var inputID = " id='colorPicker'";
+ var inputElem = document.createElement("input");
+ inputElem.setAttribute("type", inputType);
+ if (inputElem.type !== "color") {
+ // If it doesn't, fall back to text input
+ inputType = "text";
+ inputID = " ";
+ }
+
+ let input_html = "";
+ input_html += ""; // Add a default button
+ input_html += ""; // Add a reset button
+
+ var dialog = LGraphCanvas.prototype.ttNcreateDialog("" + slotType + "" +
+ input_html,
+ function(dialog, values){
+ var hexColor = values[0].toUpperCase();
+
+ if (!/^#([0-9A-F]{3}){1,2}$/i.test(hexColor)) {
+ return
+ }
+
+ if (hexColor === slotColor) {
+ return
+ }
+
+ var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {};
+ if (!customLinkColors[slotType + "_ORIG"]) {customLinkColors[slotType + "_ORIG"] = slotColor};
+ customLinkColors[slotType] = hexColor;
+ localStorage.setItem('Comfy.Settings.ttN.customLinkColors', JSON.stringify(customLinkColors));
+
+ app.canvas.default_connection_color_byType[slotType] = hexColor;
+ LGraphCanvas.link_type_colors[slotType] = hexColor;
+ }
+ );
+
+ var resetButton = dialog.querySelector("#reset");
+ resetButton.addEventListener("click", function() {
+ var colorInput = dialog.querySelector("input[type='" + inputType + "']");
+ colorInput.value = slotColor;
+ });
+
+ var defaultButton = dialog.querySelector("#Default");
+ defaultButton.addEventListener("click", function() {
+ var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {};
+ if (customLinkColors[slotType+"_ORIG"]) {
+ app.canvas.default_connection_color_byType[slotType] = customLinkColors[slotType+"_ORIG"];
+ LGraphCanvas.link_type_colors[slotType] = customLinkColors[slotType+"_ORIG"];
+
+ delete customLinkColors[slotType+"_ORIG"];
+ delete customLinkColors[slotType];
+ }
+ localStorage.setItem('Comfy.Settings.ttN.customLinkColors', JSON.stringify(customLinkColors));
+ dialog.close()
+ })
+
+ var colorPicker = dialog.querySelector("input[type='" + inputType + "']");
+ colorPicker.addEventListener("focusout", function(e) {
+ this.focus();
+ });
+ };
+
+ LGraphCanvas.prototype.ttNdefaultBGcolor = function(node, defaultBGColor){
+ setTimeout(() => {
+ if (defaultBGColor !== 'default' && !node.color) {
+ node.addProperty('ttNbgOverride', defaultBGColor);
+ node.color=defaultBGColor.color;
+ node.bgcolor=defaultBGColor.bgcolor;
+ }
+
+ if (node.color && node.properties.ttNbgOverride) {
+ if (node.properties.ttNbgOverride !== defaultBGColor && node.color === node.properties.ttNbgOverride.color) {
+ if (defaultBGColor === 'default') {
+ delete node.properties.ttNbgOverride
+ delete node.color
+ delete node.bgcolor
+ } else {
+ node.properties.ttNbgOverride = defaultBGColor
+ node.color=defaultBGColor.color;
+ node.bgcolor=defaultBGColor.bgcolor;
+ }
+ }
+
+ if (node.properties.ttNbgOverride !== defaultBGColor && node.color !== node.properties.ttNbgOverride?.color) {
+ delete node.properties.ttNbgOverride
+ }
+ }
+ }, 0);
+ };
+
+ LGraphCanvas.ttNonShowLinkStyles = function(value, options, e, menu, node) {
+ new LiteGraph.ContextMenu(
+ LiteGraph.LINK_RENDER_MODES,
+ { event: e, callback: inner_clicked, parentMenu: menu, node: node }
+ );
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+ var kV = Object.values(LiteGraph.LINK_RENDER_MODES).indexOf(v);
+
+ localStorage.setItem('Comfy.Settings.Comfy.LinkRenderMode', JSON.stringify(String(kV)));
+
+ app.canvas.links_render_mode = kV;
+ app.graph.setDirtyCanvas(true);
+ }
+
+ return false;
+ };
+
+ LGraphCanvas.ttNlinkStyleBorder = function(value, options, e, menu, node) {
+ new LiteGraph.ContextMenu(
+ [false, true],
+ { event: e, callback: inner_clicked, parentMenu: menu, node: node }
+ );
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+
+ localStorage.setItem('Comfy.Settings.ttN.links_render_border', JSON.stringify(v));
+
+ app.canvas.render_connections_border = v;
+ }
+
+ return false;
+ };
+
+ LGraphCanvas.ttNlinkStyleShadow = function(value, options, e, menu, node) {
+ new LiteGraph.ContextMenu(
+ [false, true],
+ { event: e, callback: inner_clicked, parentMenu: menu, node: node }
+ );
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+
+ localStorage.setItem('Comfy.Settings.ttN.links_render_shadow', JSON.stringify(v));
+
+ app.canvas.render_connections_shadows = v;
+ }
+
+ return false;
+ };
+
+ LGraphCanvas.ttNsetDefaultBGColor = function(value, options, e, menu, node) {
+ if (!node) {
+ throw "no node for color";
+ }
+
+ var values = [];
+ values.push({
+ value: null,
+ content:
+ "No Color"
+ });
+
+ for (var i in LGraphCanvas.node_colors) {
+ var color = LGraphCanvas.node_colors[i];
+ var value = {
+ value: i,
+ content:
+ "" +
+ i +
+ ""
+ };
+ values.push(value);
+ }
+ new LiteGraph.ContextMenu(values, {
+ event: e,
+ callback: inner_clicked,
+ parentMenu: menu,
+ node: node
+ });
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+
+ var defaultBGColor = v.value ? LGraphCanvas.node_colors[v.value] : 'default';
+
+ localStorage.setItem('Comfy.Settings.ttN.defaultBGColor', JSON.stringify(defaultBGColor));
+
+ for (var i in app.graph._nodes) {
+ LGraphCanvas.prototype.ttNdefaultBGcolor(app.graph._nodes[i], defaultBGColor);
+ }
+
+ node.setDirtyCanvas(true, true);
+ }
+
+ return false;
+ };
+
+ LGraphCanvas.ttNshowExecutionOrder = function(value, options, e, menu, node) {
+ var values = [];
+ values.push({
+ value: true,
+ content:
+ "True"
+ },
+ {
+ value: false,
+ content:
+ "False"
+ }
+ );
+
+ new LiteGraph.ContextMenu(values, {
+ event: e,
+ callback: inner_clicked,
+ parentMenu: menu,
+ node: node
+ });
+
+ function inner_clicked(v) {
+ var showExecOrder = v.value ? v.value : false;
+
+ localStorage.setItem('Comfy.Settings.ttN.showExecutionOrder', JSON.stringify(showExecOrder));
+
+ LGraphCanvas.active_canvas.render_execution_order = showExecOrder;
+
+ node.setDirtyCanvas(true, true);
+ }
+
+ return false;
+ };
+
+ const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
+ LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
+ const options = getNodeMenuOptions.apply(this, arguments);
+ node.setDirtyCanvas(true, true);
+
+ options.splice(options.length - 1, 0,
+ {
+ content: "Node Dimensions (ttN)",
+ callback: () => { LGraphCanvas.prototype.ttNsetNodeDimension(node); }
+ },
+ {
+ content: "Default BG Color (ttN)",
+ has_submenu: true,
+ callback: LGraphCanvas.ttNsetDefaultBGColor
+ },
+ {
+ content: "Show Execution Order (ttN)",
+ has_submenu: true,
+ callback: LGraphCanvas.ttNshowExecutionOrder
+
+ },
+ null
+ )
+
+ return options;
+ };
+
+ LGraphCanvas.prototype.ttNupdateRenderSettings = function (app) {
+ let showLinkBorder = Number(localStorage.getItem('Comfy.Settings.ttN.links_render_border'));
+ if (showLinkBorder !== undefined) {app.canvas.render_connections_border = showLinkBorder}
+
+ let showLinkShadow = Number(localStorage.getItem('Comfy.Settings.ttN.links_render_shadow'));
+ if (showLinkShadow !== undefined) {app.canvas.render_connections_shadows = showLinkShadow}
+
+ let showExecOrder = localStorage.getItem('Comfy.Settings.ttN.showExecutionOrder');
+ if (showExecOrder === 'true') {app.canvas.render_execution_order = true}
+ else {app.canvas.render_execution_order = false}
+
+ var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {};
+ Object.assign(app.canvas.default_connection_color_byType, customLinkColors);
+ Object.assign(LGraphCanvas.link_type_colors, customLinkColors);
+ }
+ },
+
+ beforeRegisterNodeDef(nodeType, nodeData, app) {
+ nodeType.prototype.getSlotMenuOptions = (slot) => {
+ let menu_info = [];
+ if (
+ slot &&
+ slot.output &&
+ slot.output.links &&
+ slot.output.links.length
+ ) {
+ menu_info.push({ content: "Disconnect Links", slot: slot });
+ }
+ var _slot = slot.input || slot.output;
+ if (_slot.removable){
+ menu_info.push(
+ _slot.locked
+ ? "Cannot remove"
+ : { content: "Remove Slot", slot: slot }
+ );
+ }
+ if (!_slot.nameLocked){
+ menu_info.push({ content: "Rename Slot", slot: slot });
+ }
+
+ menu_info.push({ content: "Slot Type Color (ttN)", slot: slot, callback: () => { LGraphCanvas.prototype.ttNsetSlotTypeColor(slot) } });
+ menu_info.push({ content: "Show Link Border (ttN)", has_submenu: true, slot: slot, callback: LGraphCanvas.ttNlinkStyleBorder });
+ menu_info.push({ content: "Show Link Shadow (ttN)", has_submenu: true, slot: slot, callback: LGraphCanvas.ttNlinkStyleShadow });
+ menu_info.push({ content: "Link Style (ttN)", has_submenu: true, slot: slot, callback: LGraphCanvas.ttNonShowLinkStyles });
+
+ return menu_info;
+ }
+ },
+
+ setup() {
+ LGraphCanvas.prototype.ttNupdateRenderSettings(app);
+ },
+ nodeCreated(node) {
+ let defaultBGColor = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.defaultBGColor'));
+ if (defaultBGColor) {LGraphCanvas.prototype.ttNdefaultBGcolor(node, defaultBGColor)};
+ },
+ loadedGraphNode(node, app) {
+ LGraphCanvas.prototype.ttNupdateRenderSettings(app);
+
+ let defaultBGColor = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.defaultBGColor'));
+ if (defaultBGColor) {LGraphCanvas.prototype.ttNdefaultBGcolor(node, defaultBGColor)};
+ },
+});
+
+var styleElement = document.createElement("style");
+const cssCode = `
+.ttN-dialog {
+ top: 10px;
+ left: 10px;
+ min-height: 1em;
+ background-color: var(--comfy-menu-bg);
+ font-size: 1.2em;
+ box-shadow: 0 0 7px black !important;
+ z-index: 10;
+ display: grid;
+ border-radius: 7px;
+ padding: 7px 7px;
+ position: fixed;
+}
+.ttN-dialog .name {
+ display: inline-block;
+ min-height: 1.5em;
+ font-size: 14px;
+ font-family: sans-serif;
+ color: var(--descrip-text);
+ padding: 0;
+ vertical-align: middle;
+ justify-self: center;
+}
+.ttN-dialog input,
+.ttN-dialog textarea,
+.ttN-dialog select {
+ margin: 3px;
+ min-width: 60px;
+ min-height: 1.5em;
+ background-color: var(--comfy-input-bg);
+ border: 2px solid;
+ border-color: var(--border-color);
+ color: var(--input-text);
+ border-radius: 14px;
+ padding-left: 10px;
+ outline: none;
+}
+
+.ttN-dialog #colorPicker {
+ margin: 0px;
+ min-width: 100%;
+ min-height: 2.5em;
+ border-radius: 0px;
+ padding: 0px 2px 0px 2px;
+ border: unset;
+}
+
+.ttN-dialog textarea {
+ min-height: 150px;
+}
+
+.ttN-dialog button {
+ margin-top: 3px;
+ vertical-align: top;
+ background-color: #999;
+ border: 0;
+ padding: 4px 18px;
+ border-radius: 20px;
+ cursor: pointer;
+}
+
+.ttN-dialog button.rounded,
+.ttN-dialog input.rounded {
+ border-radius: 0 12px 12px 0;
+}
+
+.ttN-dialog .helper {
+ overflow: auto;
+ max-height: 200px;
+}
+
+.ttN-dialog .help-item {
+ padding-left: 10px;
+}
+
+.ttN-dialog .help-item:hover,
+.ttN-dialog .help-item.selected {
+ cursor: pointer;
+ background-color: white;
+ color: black;
+}
+`
+styleElement.innerHTML = cssCode
+document.head.appendChild(styleElement);
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNwidgets.js b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNwidgets.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e9ef42784d9113a86b8d1b3c3efab878bd20332
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNwidgets.js
@@ -0,0 +1,403 @@
+import { app } from "../../scripts/app.js";
+import { ComfyWidgets } from "../../scripts/widgets.js";
+
+const KEY_CODES = { ENTER: 13, ESC: 27, ARROW_DOWN: 40, ARROW_UP: 38 };
+const WIDGET_GAP = -4;
+
+function hideInfoWidget(e, node, widget) {
+ let dropdownShouldBeRemoved = false;
+ let selectionIndex = -1;
+
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ displayDropdown(widget);
+ } else {
+ hideWidget(widget, node);
+ }
+
+ function createDropdownElement() {
+ const dropdown = document.createElement('ul');
+ dropdown.id = 'hideinfo-dropdown';
+ dropdown.setAttribute('role', 'listbox');
+ dropdown.classList.add('hideInfo-dropdown');
+ return dropdown;
+ }
+
+ function createDropdownItem(textContent, action) {
+ const listItem = document.createElement('li');
+ listItem.id = `hideInfo-item-${textContent.replace(/ /g, '')}`;
+ listItem.classList.add('hideInfo-item');
+ listItem.setAttribute('role', 'option');
+ listItem.textContent = textContent;
+ listItem.addEventListener('mousedown', (event) => {
+ event.preventDefault();
+ action(widget, node); // perform the action when dropdown item is clicked
+ removeDropdown();
+ dropdownShouldBeRemoved = false;
+ });
+ listItem.dataset.action = textContent.replace(/ /g, ''); // store the action in a data attribute
+ return listItem;
+ }
+
+ function displayDropdown(widget) {
+ removeDropdown();
+
+ const dropdown = createDropdownElement();
+ const listItemHide = createDropdownItem('Hide info Widget', hideWidget);
+ const listItemHideAll = createDropdownItem('Hide for all of this node-type', hideWidgetForNodetype);
+
+ dropdown.appendChild(listItemHide);
+ dropdown.appendChild(listItemHideAll);
+
+ const inputRect = widget.inputEl.getBoundingClientRect();
+ dropdown.style.top = `${inputRect.top + inputRect.height}px`;
+ dropdown.style.left = `${inputRect.left}px`;
+ dropdown.style.width = `${inputRect.width}px`;
+
+ document.body.appendChild(dropdown);
+ dropdownShouldBeRemoved = true;
+
+ widget.inputEl.removeEventListener('keydown', handleKeyDown);
+ widget.inputEl.addEventListener('keydown', handleKeyDown);
+ document.addEventListener('click', handleDocumentClick);
+ }
+
+ function removeDropdown() {
+ const dropdown = document.getElementById('hideinfo-dropdown');
+ if (dropdown) {
+ dropdown.remove();
+ widget.inputEl.removeEventListener('keydown', handleKeyDown);
+ }
+ document.removeEventListener('click', handleDocumentClick);
+
+ }
+
+ function handleKeyDown(event) {
+ const dropdownItems = document.querySelectorAll('.hideInfo-item');
+
+ if (event.keyCode === KEY_CODES.ENTER && dropdownShouldBeRemoved) {
+ event.preventDefault();
+ if (selectionIndex !== -1) {
+ const selectedAction = dropdownItems[selectionIndex].dataset.action;
+ if (selectedAction === 'HideinfoWidget') {
+ hideWidget(widget, node);
+ } else if (selectedAction === 'Hideforall') {
+ hideWidgetForNodetype(widget, node);
+ }
+ removeDropdown();
+ dropdownShouldBeRemoved = false;
+ }
+ } else if (event.keyCode === KEY_CODES.ARROW_DOWN && dropdownShouldBeRemoved) {
+ event.preventDefault();
+ if (selectionIndex !== -1) {
+ dropdownItems[selectionIndex].classList.remove('selected');
+ }
+ selectionIndex = (selectionIndex + 1) % dropdownItems.length;
+ dropdownItems[selectionIndex].classList.add('selected');
+ } else if (event.keyCode === KEY_CODES.ARROW_UP && dropdownShouldBeRemoved) {
+ event.preventDefault();
+ if (selectionIndex !== -1) {
+ dropdownItems[selectionIndex].classList.remove('selected');
+ }
+ selectionIndex = (selectionIndex - 1 + dropdownItems.length) % dropdownItems.length;
+ dropdownItems[selectionIndex].classList.add('selected');
+ } else if (event.keyCode === KEY_CODES.ESC && dropdownShouldBeRemoved) {
+ event.preventDefault();
+ removeDropdown();
+ }
+ }
+
+ function hideWidget(widget, node) {
+ node.properties['infoWidgetHidden'] = true;
+ widget.type = "ttNhidden";
+ widget.computeSize = () => [0, WIDGET_GAP];
+ node.setSize([node.size[0], node.size[1]]);
+ }
+
+ function hideWidgetForNodetype(widget, node) {
+ hideWidget(widget, node)
+ const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]");
+ if (!hiddenNodeTypes.includes(node.constructor.type)) {
+ hiddenNodeTypes.push(node.constructor.type);
+ }
+ localStorage.setItem('hiddenWidgetNodeTypes', JSON.stringify(hiddenNodeTypes));
+ }
+
+ function handleDocumentClick(event) {
+ const dropdown = document.getElementById('hideinfo-dropdown');
+
+ // If the click was outside the dropdown and the dropdown should be removed, remove it
+ if (dropdown && !dropdown.contains(event.target) && dropdownShouldBeRemoved) {
+ removeDropdown();
+ dropdownShouldBeRemoved = false;
+ }
+ }
+}
+
+
+var styleElement = document.createElement("style");
+const cssCode = `
+.ttN-info_widget {
+ background-color: var(--comfy-input-bg);
+ color: var(--input-text);
+ overflow: hidden;
+ padding: 2px;
+ resize: none;
+ border: none;
+ box-sizing: border-box;
+ font-size: 10px;
+ border-radius: 7px;
+ text-align: center;
+ text-wrap: balance;
+ text-transform: uppercase;
+}
+.hideInfo-dropdown {
+ position: absolute;
+ box-sizing: border-box;
+ background-color: #121212;
+ border-radius: 7px;
+ box-shadow: 0 2px 4px rgba(255, 255, 255, .25);
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ z-index: 1000;
+ overflow: auto;
+ max-height: 200px;
+}
+
+.hideInfo-dropdown li {
+ padding: 4px 10px;
+ cursor: pointer;
+ font-family: system-ui;
+ font-size: 0.7rem;
+}
+
+.hideInfo-dropdown li:hover,
+.hideInfo-dropdown li.selected {
+ background-color: #e5e5e5;
+ border-radius: 7px;
+}
+`
+styleElement.innerHTML = cssCode
+document.head.appendChild(styleElement);
+
+const InfoSymbol = Symbol();
+const InfoResizeSymbol = Symbol();
+
+
+
+
+// WIDGET FUNCTIONS
+function addInfoWidget(node, name, opts, app) {
+ const INFO_W_SIZE = 50;
+
+ node.addProperty('infoWidgetHidden', false)
+
+ function computeSize(size) {
+ if (node.widgets[0].last_y == null) return;
+
+ let y = node.widgets[0].last_y;
+
+ // Compute the height of all non ttNinfo widgets
+ let widgetHeight = 0;
+ const infoWidges = [];
+ for (let i = 0; i < node.widgets.length; i++) {
+ const w = node.widgets[i];
+ if (w.type === "ttNinfo") {
+ infoWidges.push(w);
+ } else {
+ if (w.computeSize) {
+ widgetHeight += w.computeSize()[1] + 4;
+ } else {
+ widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
+ }
+ }
+ }
+
+ let infoWidgetSpace = infoWidges.length * INFO_W_SIZE; // Height for all info widgets
+
+ // Check if there's enough space for all widgets
+ if (size[1] < y + widgetHeight + infoWidgetSpace) {
+ // There isn't enough space for all the widgets, increase the size of the node
+ node.size[1] = y + widgetHeight + infoWidgetSpace;
+ node.graph.setDirtyCanvas(true);
+ }
+
+ // Position each of the widgets
+ for (const w of node.widgets) {
+ w.y = y;
+ if (w.type === "ttNinfo") {
+ y += INFO_W_SIZE;
+ } else if (w.computeSize) {
+ y += w.computeSize()[1] + 4;
+ } else {
+ y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
+ }
+ }
+ }
+
+ const widget = {
+ type: "ttNinfo",
+ name,
+ get value() {
+ return this.inputEl.value;
+ },
+ set value(x) {
+ this.inputEl.value = x;
+ },
+ draw: function (ctx, _, widgetWidth, y, widgetHeight) {
+ if (!this.parent.inputHeight) {
+ // If we are initially offscreen when created we wont have received a resize event
+ // Calculate it here instead
+ computeSize(node.size);
+ }
+ const visible = app.canvas.ds.scale > 0.5 && this.type === "ttNinfo";
+ const margin = 10;
+ const elRect = ctx.canvas.getBoundingClientRect();
+ const transform = new DOMMatrix()
+ .scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
+ .multiplySelf(ctx.getTransform())
+ .translateSelf(margin, margin + y);
+
+ Object.assign(this.inputEl.style, {
+ transformOrigin: "0 0",
+ transform: transform,
+ left: "0px",
+ top: "0px",
+ width: `${widgetWidth - (margin * 2)}px`,
+ height: `${this.parent.inputHeight - (margin * 2)}px`,
+ position: "absolute",
+ background: (!node.color)?'':node.color,
+ color: (!node.color)?'':'white',
+ zIndex: app.graph._nodes.indexOf(node),
+ });
+ this.inputEl.hidden = !visible;
+ },
+ };
+ widget.inputEl = document.createElement("textarea");
+ widget.inputEl.className = "ttN-info_widget";
+ widget.inputEl.value = opts.defaultVal;
+ widget.inputEl.placeholder = opts.placeholder || "";
+ widget.inputEl.readOnly = true;
+ widget.parent = node;
+
+ document.body.appendChild(widget.inputEl);
+
+ node.addCustomWidget(widget);
+
+ app.canvas.onDrawBackground = function () {
+ // Draw node isnt fired once the node is off the screen
+ // if it goes off screen quickly, the input may not be removed
+ // this shifts it off screen so it can be moved back if the node is visible.
+ for (let n in app.graph._nodes) {
+ n = graph._nodes[n];
+ for (let w in n.widgets) {
+ let wid = n.widgets[w];
+ if (Object.hasOwn(wid, "inputEl")) {
+ wid.inputEl.style.left = -8000 + "px";
+ wid.inputEl.style.position = "absolute";
+ }
+ }
+ }
+ };
+
+ node.onRemoved = function () {
+ // When removing this node we need to remove the input from the DOM
+ for (let y in this.widgets) {
+ if (this.widgets[y].inputEl) {
+ this.widgets[y].inputEl.remove();
+ }
+ }
+ };
+
+ widget.onRemove = () => {
+ widget.inputEl?.remove();
+
+ // Restore original size handler if we are the last
+ if (!--node[InfoSymbol]) {
+ node.onResize = node[InfoResizeSymbol];
+ delete node[InfoSymbol];
+ delete node[InfoResizeSymbol];
+ }
+ };
+
+ if (node[InfoSymbol]) {
+ node[InfoSymbol]++;
+ } else {
+ node[InfoSymbol] = 1;
+ const onResize = (node[InfoResizeSymbol] = node.onResize);
+
+ node.onResize = function (size) {
+ computeSize(size);
+
+ // Call original resizer handler
+ if (onResize) {
+ console.log(this, arguments)
+ onResize.apply(this, arguments);
+ }
+ };
+ }
+
+ return { widget };
+}
+
+// WIDGETS
+const ttNcustomWidgets = {
+ INFO(node, inputName, inputData, app) {
+ const defaultVal = inputData[1].default || "";
+ return addInfoWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
+ },
+}
+
+
+
+app.registerExtension({
+ name: "comfy.ttN.widgets",
+ getCustomWidgets(app) {
+ return ttNcustomWidgets;
+ },
+ nodeCreated(node) {
+ if (node.widgets) {
+ // Locate info widgets
+ const widgets = node.widgets.filter(
+ (n) => (n.type === "ttNinfo")
+ );
+ for (const widget of widgets) {
+ widget.inputEl.addEventListener('contextmenu', function(e) {
+ hideInfoWidget(e, node, widget);
+ });
+ widget.inputEl.addEventListener('click', function(e) {
+ hideInfoWidget(e, node, widget);
+ });
+ }
+ }
+ },
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]");
+ const origOnConfigure = nodeType.prototype.onConfigure;
+ nodeType.prototype.onConfigure = function () {
+ const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined;
+ if (this.properties['infoWidgetHidden']) {
+ for (let i in this.widgets) {
+ if (this.widgets[i].type == "ttNinfo") {
+ hideInfoWidget(null, this, this.widgets[i]);
+ }
+ }
+ }
+ return r;
+ };
+ const origOnAdded = nodeType.prototype.onAdded;
+ nodeType.prototype.onAdded = function () {
+ const r = origOnAdded ? origOnAdded.apply(this, arguments) : undefined;
+ if (hiddenNodeTypes.includes(this.type)) {
+ for (let i in this.widgets) {
+ if (this.widgets[i].type == "ttNinfo") {
+ this.properties['infoWidgetHidden'] = true;
+ }
+ }
+ }
+ return r;
+ }
+ }
+});
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNxyPlot.js b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNxyPlot.js
new file mode 100644
index 0000000000000000000000000000000000000000..84f03590e869694ee06b13c49d2e8255ecf7dde5
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/js/ttNxyPlot.js
@@ -0,0 +1,212 @@
+import { app } from "../../scripts/app.js";
+import { ttN_CreateDropdown, ttN_RemoveDropdown } from "./ttN.js";
+
+function generateNumList(dictionary) {
+ const minimum = dictionary["min"] || 0;
+ const maximum = dictionary["max"] || 0;
+ const step = dictionary["step"] || 1;
+
+ if (step === 0) {
+ return [];
+ }
+
+ const result = [];
+ let currentValue = minimum;
+
+ while (currentValue <= maximum) {
+ if (Number.isInteger(step)) {
+ result.push(Math.round(currentValue) + '; ');
+ } else {
+ let formattedValue = currentValue.toFixed(3);
+ if(formattedValue == -0.000){
+ formattedValue = '0.000';
+ }
+ if (!/\.\d{3}$/.test(formattedValue)) {
+ formattedValue += "0";
+ }
+ result.push(formattedValue + "; ");
+ }
+ currentValue += step;
+ }
+
+ if (maximum >= 0 && minimum >= 0) {
+ //low to high
+ return result;
+ }
+ else {
+ //high to low
+ return result.reverse();
+ }
+}
+
+let plotDict = {};
+let currentOptionsDict = {};
+
+function getCurrentOptionLists(node, widget) {
+ const nodeId = String(node.id);
+ const widgetName = widget.name;
+ const widgetValue = widget.value.replace(/^(loader|sampler):\s/, '');
+
+ if (!currentOptionsDict[nodeId] || !currentOptionsDict[nodeId][widgetName]) {
+ currentOptionsDict[nodeId] = {...currentOptionsDict[nodeId], [widgetName]: plotDict[widgetValue]};
+ } else if (currentOptionsDict[nodeId][widgetName] != plotDict[widgetValue]) {
+ currentOptionsDict[nodeId][widgetName] = plotDict[widgetValue];
+ }
+}
+
+function addGetSetters(node) {
+ if (node.widgets)
+ for (const w of node.widgets) {
+ if (w.name === "x_axis" ||
+ w.name === "y_axis") {
+ let widgetValue = w.value;
+
+ // Define getters and setters for widget values
+ Object.defineProperty(w, 'value', {
+
+ get() {
+ return widgetValue;
+ },
+ set(newVal) {
+ if (newVal !== widgetValue) {
+ widgetValue = newVal;
+ getCurrentOptionLists(node, w);
+ }
+ }
+ });
+ }
+ }
+}
+
+function dropdownCreator(node) {
+ if (node.widgets) {
+ const widgets = node.widgets.filter(
+ (n) => (n.type === "customtext" && n.dynamicPrompts !== false) || n.dynamicPrompts
+ );
+
+ for (const w of widgets) {
+ function replaceOptionSegments(selectedOption, inputSegments, cursorSegmentIndex, optionsList) {
+ if (selectedOption) {
+ inputSegments[cursorSegmentIndex] = selectedOption;
+ }
+
+ return inputSegments.map(segment => verifySegment(segment, optionsList))
+ .filter(item => item !== '')
+ .join('');
+ }
+
+ function verifySegment(segment, optionsList) {
+ segment = cleanSegment(segment);
+
+ if (isInOptionsList(segment, optionsList)) {
+ return segment + '; ';
+ }
+
+ let matchedOptions = findMatchedOptions(segment, optionsList);
+
+ if (matchedOptions.length === 1 || matchedOptions.length === 2) {
+ return matchedOptions[0];
+ }
+
+ if (isInOptionsList(formatNumberSegment(segment), optionsList)) {
+ return formatNumberSegment(segment) + '; ';
+ }
+
+ return '';
+ }
+
+ function cleanSegment(segment) {
+ return segment.replace(/(\n|;| )/g, '');
+ }
+
+ function isInOptionsList(segment, optionsList) {
+ return optionsList.includes(segment + '; ');
+ }
+
+ function findMatchedOptions(segment, optionsList) {
+ return optionsList.filter(option => option.toLowerCase().includes(segment.toLowerCase()));
+ }
+
+ function formatNumberSegment(segment) {
+ if (Number(segment)) {
+ return Number(segment).toFixed(3);
+ }
+
+ if (['0', '0.', '0.0', '0.00', '00'].includes(segment)) {
+ return '0.000';
+ }
+ return segment;
+ }
+
+
+ const onInput = function () {
+ const nodeId = String(w.parent.id);
+ const axisWidgetName = w.name[0] + '_axis';
+
+ let optionsList = currentOptionsDict[nodeId]?.[axisWidgetName] || [];
+ if (optionsList.length === 0) {return}
+
+ const inputText = w.inputEl.value;
+ const cursorPosition = w.inputEl.selectionStart;
+
+ let inputSegments = inputText.split('; ');
+
+ const cursorSegmentIndex = inputText.substring(0, cursorPosition).split('; ').length - 1;
+ const currentSegment = inputSegments[cursorSegmentIndex];
+ const currentSegmentLower = currentSegment.replace(/\n/g, '').toLowerCase();
+
+ const filteredOptionsList = optionsList.filter(option => option.toLowerCase().includes(currentSegmentLower)).map(option => option.replace(/; /g, ''));
+
+ if (filteredOptionsList.length > 0) {
+ ttN_CreateDropdown(w.inputEl, filteredOptionsList, (selectedOption) => {
+ const verifiedText = replaceOptionSegments(selectedOption, inputSegments, cursorSegmentIndex, optionsList);
+ w.inputEl.value = verifiedText;
+ });
+ }
+ else {
+ ttN_RemoveDropdown();
+ const verifiedText = replaceOptionSegments(null, inputSegments, cursorSegmentIndex, optionsList);
+ w.inputEl.value = verifiedText;
+ }
+ };
+
+ w.inputEl.removeEventListener('input', onInput);
+ w.inputEl.addEventListener('input', onInput);
+ w.inputEl.removeEventListener('mouseup', onInput);
+ w.inputEl.addEventListener('mouseup', onInput);
+ }
+ }
+}
+
+app.registerExtension({
+ name: "comfy.ttN.xyPlot",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === "ttN xyPlot") {
+ plotDict = nodeData.input.hidden.plot_dict[0];
+
+ for (const key in plotDict) {
+ const value = plotDict[key];
+ if (Array.isArray(value)) {
+ let updatedValues = [];
+ for (const v of value) {
+ updatedValues.push(v + '; ');
+ }
+ plotDict[key] = updatedValues;
+ } else if (typeof(value) === 'object') {
+ plotDict[key] = generateNumList(value);
+ } else {
+ plotDict[key] = value + '; ';
+ }
+ }
+ plotDict["None"] = [];
+ plotDict["---------------------"] = [];
+ }
+ },
+ nodeCreated(node) {
+ if (node.getTitle() === "xyPlot") {
+ addGetSetters(node);
+ dropdownCreator(node);
+
+ }
+ }
+});
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/tinyterraNodes.py b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/tinyterraNodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..9676d6c206ee68c03dc48efa33266904ce123476
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/tinyterraNodes.py
@@ -0,0 +1,3236 @@
+"""
+@author: tinyterra
+@title: tinyterraNodes
+@nickname: ttNodes
+@description: This extension offers various pipe nodes, fullscreen image viewer based on node history, dynamic widgets, interface customization, and more.
+"""
+
+#---------------------------------------------------------------------------------------------------------------------------------------------------#
+# tinyterraNodes developed in 2023 by tinyterra https://github.com/TinyTerra #
+# for ComfyUI https://github.com/comfyanonymous/ComfyUI #
+# Like the pack and want to support me? https://www.buymeacoffee.com/tinyterra #
+#---------------------------------------------------------------------------------------------------------------------------------------------------#
+
+ttN_version = '1.2.0'
+
+MAX_RESOLUTION=8192
+
+import os
+import re
+import json
+import time
+import torch
+import psutil
+import random
+import datetime
+import comfy.sd
+import comfy.utils
+import numpy as np
+import folder_paths
+import comfy.samplers
+import latent_preview
+import comfy.model_base
+from pathlib import Path
+import comfy.model_management
+from comfy.sd import CLIP, VAE
+from comfy.cli_args import args
+from urllib.request import urlopen
+from collections import defaultdict
+from PIL.PngImagePlugin import PngInfo
+from PIL import Image, ImageDraw, ImageFont
+from comfy.model_patcher import ModelPatcher
+from comfy_extras.chainner_models import model_loading
+from typing import Dict, List, Optional, Tuple, Union, Any
+from .adv_encode import advanced_encode, advanced_encode_XL
+
+class CC:
+ CLEAN = '\33[0m'
+ BOLD = '\33[1m'
+ ITALIC = '\33[3m'
+ UNDERLINE = '\33[4m'
+ BLINK = '\33[5m'
+ BLINK2 = '\33[6m'
+ SELECTED = '\33[7m'
+
+ BLACK = '\33[30m'
+ RED = '\33[31m'
+ GREEN = '\33[32m'
+ YELLOW = '\33[33m'
+ BLUE = '\33[34m'
+ VIOLET = '\33[35m'
+ BEIGE = '\33[36m'
+ WHITE = '\33[37m'
+
+ GREY = '\33[90m'
+ LIGHTRED = '\33[91m'
+ LIGHTGREEN = '\33[92m'
+ LIGHTYELLOW = '\33[93m'
+ LIGHTBLUE = '\33[94m'
+ LIGHTVIOLET = '\33[95m'
+ LIGHTBEIGE = '\33[96m'
+ LIGHTWHITE = '\33[97m'
+
+class ttNl:
+ def __init__(self, input_string):
+ self.header_value = f'{CC.LIGHTGREEN}[ttN] {CC.GREEN}'
+ self.label_value = ''
+ self.title_value = ''
+ self.input_string = f'{input_string}{CC.CLEAN}'
+
+ def h(self, header_value):
+ self.header_value = f'{CC.LIGHTGREEN}[{header_value}] {CC.GREEN}'
+ return self
+
+ def full(self):
+ self.h('tinyterraNodes')
+ return self
+
+ def success(self):
+ self.label_value = f'Success: '
+ return self
+
+ def warn(self):
+ self.label_value = f'{CC.RED}Warning:{CC.LIGHTRED} '
+ return self
+
+ def error(self):
+ self.label_value = f'{CC.LIGHTRED}ERROR:{CC.RED} '
+ return self
+
+ def t(self, title_value):
+ self.title_value = f'{title_value}:{CC.CLEAN} '
+ return self
+
+ def p(self):
+ print(self.header_value + self.label_value + self.title_value + self.input_string)
+ return self
+
+ def interrupt(self, msg):
+ raise Exception(msg)
+
+class ttNpaths:
+ ComfyUI = folder_paths.base_path
+ tinyterraNodes = Path(__file__).parent
+ font_path = os.path.join(tinyterraNodes, 'arial.ttf')
+
+class ttNloader:
+ def __init__(self):
+ self.loaded_objects = {
+ "ckpt": defaultdict(tuple), # {ckpt_name: (model, ...)}
+ "clip": defaultdict(tuple),
+ "bvae": defaultdict(tuple),
+ "vae": defaultdict(object),
+ "lora": defaultdict(dict), # {lora_name: {UID: (model_lora, clip_lora)}}
+ }
+ self.memory_threshold = self.determine_memory_threshold(0.7)
+
+ def clean_values(self, values: str):
+ original_values = values.split("; ")
+ cleaned_values = []
+
+ for value in original_values:
+ cleaned_value = value.strip(';').strip()
+
+ if cleaned_value == "":
+ continue
+
+ try:
+ cleaned_value = int(cleaned_value)
+ except ValueError:
+ try:
+ cleaned_value = float(cleaned_value)
+ except ValueError:
+ pass
+
+ cleaned_values.append(cleaned_value)
+
+ return cleaned_values
+
+ def clear_unused_objects(self, desired_names: set, object_type: str):
+ keys = set(self.loaded_objects[object_type].keys())
+ for key in keys - desired_names:
+ del self.loaded_objects[object_type][key]
+
+ def get_input_value(self, entry, key):
+ val = entry["inputs"][key]
+ return val if isinstance(val, str) else val[0]
+
+ def process_pipe_loader(self, entry,
+ desired_ckpt_names, desired_vae_names,
+ desired_lora_names, desired_lora_settings, num_loras=3, suffix=""):
+ for idx in range(1, num_loras + 1):
+ lora_name_key = f"{suffix}lora{idx}_name"
+ desired_lora_names.add(self.get_input_value(entry, lora_name_key))
+ setting = f'{self.get_input_value(entry, lora_name_key)};{entry["inputs"][f"{suffix}lora{idx}_model_strength"]};{entry["inputs"][f"{suffix}lora{idx}_clip_strength"]}'
+ desired_lora_settings.add(setting)
+
+ desired_ckpt_names.add(self.get_input_value(entry, f"{suffix}ckpt_name"))
+ desired_vae_names.add(self.get_input_value(entry, f"{suffix}vae_name"))
+
+ def update_loaded_objects(self, prompt):
+ desired_ckpt_names = set()
+ desired_vae_names = set()
+ desired_lora_names = set()
+ desired_lora_settings = set()
+
+ for entry in prompt.values():
+ class_type = entry["class_type"]
+
+ if class_type == "ttN pipeLoader":
+ self.process_pipe_loader(entry, desired_ckpt_names=desired_ckpt_names,
+ desired_vae_names=desired_vae_names,
+ desired_lora_names=desired_lora_names,
+ desired_lora_settings=desired_lora_settings)
+
+ elif class_type == "ttN pipeLoaderSDXL":
+ self.process_pipe_loader(entry, num_loras=2, suffix="refiner_", desired_ckpt_names=desired_ckpt_names, desired_vae_names=desired_vae_names, desired_lora_names=desired_lora_names, desired_lora_settings=desired_lora_settings)
+ self.process_pipe_loader(entry, num_loras=2, desired_ckpt_names=desired_ckpt_names, desired_vae_names=desired_vae_names, desired_lora_names=desired_lora_names, desired_lora_settings=desired_lora_settings)
+
+ elif class_type == "ttN pipeKSampler" or class_type == "ttN pipeKSamplerAdvanced":
+ lora_name = self.get_input_value(entry, "lora_name")
+ desired_lora_names.add(lora_name)
+ setting = f'{lora_name};{entry["inputs"]["lora_model_strength"]};{entry["inputs"]["lora_clip_strength"]}'
+ desired_lora_settings.add(setting)
+
+ elif class_type == "ttN xyPlot":
+ for axis in ["x", "y"]:
+ axis_key = f"{axis}_axis"
+ if entry["inputs"][axis_key] != "None":
+ axis_entry = entry["inputs"][axis_key].split(": ")[1]
+ vals = self.clean_values(entry["inputs"][f"{axis}_values"])
+ desired_names_set = {
+ "vae_name": desired_vae_names,
+ "ckpt_name": desired_ckpt_names,
+ "lora_name": desired_lora_names,
+ "lora1_name": desired_lora_names,
+ "lora2_name": desired_lora_names,
+ "lora3_name": desired_lora_names,
+ }
+ if desired_names_set.get(axis_entry) is not None:
+ desired_names_set[axis_entry].update(vals)
+
+ elif class_type == "ttN multiModelMerge":
+ for letter in "ABC":
+ desired_ckpt_names.add(self.get_input_value(entry, f"ckpt_{letter}_name"))
+
+ object_types = ["ckpt", "clip", "bvae", "vae", "lora"]
+ for object_type in object_types:
+ desired_names = desired_ckpt_names if object_type in ["ckpt", "clip", "bvae"] else desired_vae_names if object_type == "vae" else desired_lora_names
+ self.clear_unused_objects(desired_names, object_type)
+
+ def add_to_cache(self, obj_type, key, value):
+ """
+ Add an item to the cache with the current timestamp.
+ """
+ timestamped_value = (value, time.time())
+ self.loaded_objects[obj_type][key] = timestamped_value
+
+
+ def determine_memory_threshold(self, percentage=0.8):
+ """
+ Determines the memory threshold as a percentage of the total available memory.
+
+ Args:
+ - percentage (float): The fraction of total memory to use as the threshold.
+ Should be a value between 0 and 1. Default is 0.8 (80%).
+
+ Returns:
+ - memory_threshold (int): Memory threshold in bytes.
+ """
+ total_memory = psutil.virtual_memory().total
+ memory_threshold = total_memory * percentage
+ return memory_threshold
+
+ def get_memory_usage(self):
+ """
+ Returns the memory usage of the current process in bytes.
+ """
+ process = psutil.Process(os.getpid())
+ return process.memory_info().rss
+
+ def eviction_based_on_memory(self):
+ """
+ Evicts objects from cache based on memory usage and priority.
+ """
+ current_memory = self.get_memory_usage()
+
+ if current_memory < self.memory_threshold:
+ return
+
+ eviction_order = ["vae", "lora", "bvae", "clip", "ckpt"]
+
+ for obj_type in eviction_order:
+ if current_memory < self.memory_threshold:
+ break
+
+ # Sort items based on age (using the timestamp)
+ items = list(self.loaded_objects[obj_type].items())
+ items.sort(key=lambda x: x[1][1]) # Sorting by timestamp
+
+ for item in items:
+ if current_memory < self.memory_threshold:
+ break
+
+ del self.loaded_objects[obj_type][item[0]]
+ current_memory = self.get_memory_usage()
+
+ def load_checkpoint(self, ckpt_name, config_name=None):
+ cache_name = ckpt_name
+ if config_name not in [None, "Default"]:
+ cache_name = ckpt_name + "_" + config_name
+ if cache_name in self.loaded_objects["ckpt"]:
+ return self.loaded_objects["ckpt"][cache_name][0], self.loaded_objects["clip"][cache_name][0], self.loaded_objects["bvae"][cache_name][0]
+
+ ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
+
+ if config_name not in [None, "Default"]:
+ config_path = folder_paths.get_full_path("configs", config_name)
+ loaded_ckpt = comfy.sd.load_checkpoint(config_path, ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
+ else:
+ loaded_ckpt = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
+
+ self.add_to_cache("ckpt", cache_name, loaded_ckpt[0])
+ self.add_to_cache("clip", cache_name, loaded_ckpt[1])
+ self.add_to_cache("bvae", cache_name, loaded_ckpt[2])
+
+ self.eviction_based_on_memory()
+
+
+ return loaded_ckpt[0], loaded_ckpt[1], loaded_ckpt[2]
+
+ def load_vae(self, vae_name):
+ if vae_name in self.loaded_objects["vae"]:
+ return self.loaded_objects["vae"][vae_name][0]
+
+ vae_path = folder_paths.get_full_path("vae", vae_name)
+ sd = comfy.utils.load_torch_file(vae_path)
+ loaded_vae = comfy.sd.VAE(sd=sd)
+ self.add_to_cache("vae", vae_name, loaded_vae)
+ self.eviction_based_on_memory()
+
+ return loaded_vae
+
+ def load_lora(self, lora_name, model, clip, strength_model, strength_clip):
+ model_hash = str(model)[44:-1]
+ clip_hash = str(clip)[25:-1]
+
+ unique_id = f'{model_hash};{clip_hash};{lora_name};{strength_model};{strength_clip}'
+
+ if unique_id in self.loaded_objects["lora"] and unique_id in self.loaded_objects["lora"][lora_name]:
+ return self.loaded_objects["lora"][unique_id][0]
+
+ lora_path = folder_paths.get_full_path("loras", lora_name)
+ lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
+ model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip)
+
+ self.add_to_cache("lora", unique_id, (model_lora, clip_lora))
+ self.eviction_based_on_memory()
+
+ return model_lora, clip_lora
+
+class ttNsampler:
+ def __init__(self):
+ self.last_helds: dict[str, list] = {
+ "results": [],
+ "pipe_line": [],
+ }
+
+ @staticmethod
+ def tensor2pil(image: torch.Tensor) -> Image.Image:
+ """Convert a torch tensor to a PIL image."""
+ return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
+
+ @staticmethod
+ def pil2tensor(image: Image.Image) -> torch.Tensor:
+ """Convert a PIL image to a torch tensor."""
+ return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
+
+ @staticmethod
+ def enforce_mul_of_64(d):
+ d = int(d)
+ if d<=7:
+ d = 8
+ leftover = d % 8 # 8 is the number of pixels per byte
+ if leftover != 0: # if the number of pixels is not a multiple of 8
+ if (leftover < 4): # if the number of pixels is less than 4
+ d -= leftover # remove the leftover pixels
+ else: # if the number of pixels is more than 4
+ d += 8 - leftover # add the leftover pixels
+
+ return int(d)
+
+ @staticmethod
+ def safe_split(to_split: str, delimiter: str) -> List[str]:
+ """Split the input string and return a list of non-empty parts."""
+ parts = to_split.split(delimiter)
+ parts = [part for part in parts if part not in ('', ' ', ' ')]
+
+ while len(parts) < 2:
+ parts.append('None')
+ return parts
+
+ def common_ksampler(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0, disable_noise=False, start_step=None, last_step=None, force_full_denoise=False, preview_latent=True, disable_pbar=False):
+ device = comfy.model_management.get_torch_device()
+ latent_image = latent["samples"]
+
+ if disable_noise:
+ noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
+ else:
+ batch_inds = latent["batch_index"] if "batch_index" in latent else None
+ noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
+
+ noise_mask = None
+ if "noise_mask" in latent:
+ noise_mask = latent["noise_mask"]
+
+ preview_format = "JPEG"
+ if preview_format not in ["JPEG", "PNG"]:
+ preview_format = "JPEG"
+
+ previewer = False
+
+ if preview_latent:
+ previewer = latent_preview.get_previewer(device, model.model.latent_format)
+
+ pbar = comfy.utils.ProgressBar(steps)
+ def callback(step, x0, x, total_steps):
+ preview_bytes = None
+ if previewer:
+ preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0)
+ pbar.update_absolute(step + 1, total_steps, preview_bytes)
+
+ samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
+ denoise=denoise, disable_noise=disable_noise, start_step=start_step, last_step=last_step,
+ force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=seed)
+
+ out = latent.copy()
+ out["samples"] = samples
+ return out
+
+ def get_value_by_id(self, key: str, my_unique_id: Any) -> Optional[Any]:
+ """Retrieve value by its associated ID."""
+ try:
+ for value, id_ in self.last_helds[key]:
+ if id_ == my_unique_id:
+ return value
+ except KeyError:
+ return None
+
+ def update_value_by_id(self, key: str, my_unique_id: Any, new_value: Any) -> Union[bool, None]:
+ """Update the value associated with a given ID. Return True if updated, False if appended, None if key doesn't exist."""
+ try:
+ for i, (value, id_) in enumerate(self.last_helds[key]):
+ if id_ == my_unique_id:
+ self.last_helds[key][i] = (new_value, id_)
+ return True
+ self.last_helds[key].append((new_value, my_unique_id))
+ return False
+ except KeyError:
+ return False
+
+ def upscale(self, samples, upscale_method, scale_by, crop):
+ s = samples.copy()
+ width = self.enforce_mul_of_64(round(samples["samples"].shape[3] * scale_by))
+ height = self.enforce_mul_of_64(round(samples["samples"].shape[2] * scale_by))
+
+ if (width > MAX_RESOLUTION):
+ width = MAX_RESOLUTION
+ if (height > MAX_RESOLUTION):
+ height = MAX_RESOLUTION
+
+ s["samples"] = comfy.utils.common_upscale(samples["samples"], width, height, upscale_method, crop)
+ return (s,)
+
+ def handle_upscale(self, samples: dict, upscale_method: str, factor: float, crop: bool) -> dict:
+ """Upscale the samples if the upscale_method is not set to 'None'."""
+ if upscale_method != "None":
+ samples = self.upscale(samples, upscale_method, factor, crop)[0]
+ return samples
+
+ def init_state(self, my_unique_id: Any, key: str, default: Any) -> Any:
+ """Initialize the state by either fetching the stored value or setting a default."""
+ value = self.get_value_by_id(key, my_unique_id)
+ if value is not None:
+ return value
+ return default
+
+ def get_output(self, pipe: dict) -> Tuple:
+ """Return a tuple of various elements fetched from the input pipe dictionary."""
+ return (
+ pipe,
+ pipe.get("model"),
+ pipe.get("positive"),
+ pipe.get("negative"),
+ pipe.get("samples"),
+ pipe.get("vae"),
+ pipe.get("clip"),
+ pipe.get("images"),
+ pipe.get("seed")
+ )
+
+ def get_output_sdxl(self, sdxl_pipe: dict) -> Tuple:
+ """Return a tuple of various elements fetched from the input sdxl_pipe dictionary."""
+ return (
+ sdxl_pipe,
+ sdxl_pipe.get("model"),
+ sdxl_pipe.get("positive"),
+ sdxl_pipe.get("negative"),
+ sdxl_pipe.get("vae"),
+ sdxl_pipe.get("refiner_model"),
+ sdxl_pipe.get("refiner_positive"),
+ sdxl_pipe.get("refiner_negative"),
+ sdxl_pipe.get("refiner_vae"),
+ sdxl_pipe.get("samples"),
+ sdxl_pipe.get("clip"),
+ sdxl_pipe.get("images"),
+ sdxl_pipe.get("seed")
+ )
+
+class ttNxyPlot:
+ def __init__(self, xyPlotData, save_prefix, image_output, prompt, extra_pnginfo, my_unique_id):
+ self.x_node_type, self.x_type = ttNsampler.safe_split(xyPlotData.get("x_axis"), ': ')
+ self.y_node_type, self.y_type = ttNsampler.safe_split(xyPlotData.get("y_axis"), ': ')
+
+ self.x_values = xyPlotData.get("x_vals") if self.x_type != "None" else []
+ self.y_values = xyPlotData.get("y_vals") if self.y_type != "None" else []
+
+ self.grid_spacing = xyPlotData.get("grid_spacing")
+ self.latent_id = xyPlotData.get("latent_id")
+ self.output_individuals = xyPlotData.get("output_individuals")
+
+ self.x_label, self.y_label = [], []
+ self.max_width, self.max_height = 0, 0
+ self.latents_plot = []
+ self.image_list = []
+
+ self.num_cols = len(self.x_values) if len(self.x_values) > 0 else 1
+ self.num_rows = len(self.y_values) if len(self.y_values) > 0 else 1
+
+ self.total = self.num_cols * self.num_rows
+ self.num = 0
+
+ self.save_prefix = save_prefix
+ self.image_output = image_output
+ self.prompt = prompt
+ self.extra_pnginfo = extra_pnginfo
+ self.my_unique_id = my_unique_id
+
+ # Helper Functions
+ @staticmethod
+ def define_variable(plot_image_vars, value_type, value, index):
+ value_label = f"{value}"
+ if value_type == "seed":
+ seed = int(plot_image_vars["seed"])
+ if index != 0:
+ index = 1
+ if value == 'increment':
+ plot_image_vars["seed"] = seed + index
+ value_label = f"{plot_image_vars['seed']}"
+
+ elif value == 'decrement':
+ plot_image_vars["seed"] = seed - index
+ value_label = f"{plot_image_vars['seed']}"
+
+ elif value == 'randomize':
+ plot_image_vars["seed"] = random.randint(0, 0xffffffffffffffff)
+ value_label = f"{plot_image_vars['seed']}"
+ else:
+ plot_image_vars[value_type] = value
+
+ if value_type in ["steps", "cfg", "denoise", "clip_skip",
+ "lora1_model_strength", "lora1_clip_strength",
+ "lora2_model_strength", "lora2_clip_strength",
+ "lora3_model_strength", "lora3_clip_strength"]:
+ value_label = f"{value_type}: {value}"
+
+ if value_type in ["lora_model&clip_strength", "lora1_model&clip_strength", "lora2_model&clip_strength", "lora3_model&clip_strength"]:
+ loraNum = value_type.split("_")[0]
+ plot_image_vars[loraNum + "_model_strength"] = value
+ plot_image_vars[loraNum + "_clip_strength"] = value
+
+ type_label = value_type.replace("_model&clip", "")
+ value_label = f"{type_label}: {value}"
+
+ elif value_type == "positive_token_normalization":
+ value_label = f'(+) token norm.: {value}'
+ elif value_type == "positive_weight_interpretation":
+ value_label = f'(+) weight interp.: {value}'
+ elif value_type == "negative_token_normalization":
+ value_label = f'(-) token norm.: {value}'
+ elif value_type == "negative_weight_interpretation":
+ value_label = f'(-) weight interp.: {value}'
+
+ elif value_type == "positive":
+ value_label = f"pos prompt {index + 1}"
+ elif value_type == "negative":
+ value_label = f"neg prompt {index + 1}"
+
+ return plot_image_vars, value_label
+
+ @staticmethod
+ def get_font(font_size):
+ return ImageFont.truetype(str(Path(ttNpaths.font_path)), font_size)
+
+ @staticmethod
+ def update_label(label, value, num_items):
+ if len(label) < num_items:
+ return [*label, value]
+ return label
+
+ @staticmethod
+ def rearrange_tensors(latent, num_cols, num_rows):
+ new_latent = []
+ for i in range(num_rows):
+ for j in range(num_cols):
+ index = j * num_rows + i
+ new_latent.append(latent[index])
+ return new_latent
+
+ def calculate_background_dimensions(self):
+ border_size = int((self.max_width//8)*1.5) if self.y_type != "None" or self.x_type != "None" else 0
+ bg_width = self.num_cols * (self.max_width + self.grid_spacing) - self.grid_spacing + border_size * (self.y_type != "None")
+ bg_height = self.num_rows * (self.max_height + self.grid_spacing) - self.grid_spacing + border_size * (self.x_type != "None")
+
+ x_offset_initial = border_size if self.y_type != "None" else 0
+ y_offset = border_size if self.x_type != "None" else 0
+
+ return bg_width, bg_height, x_offset_initial, y_offset
+
+ def adjust_font_size(self, text, initial_font_size, label_width):
+ font = self.get_font(initial_font_size)
+ text_width, _ = font.getsize(text)
+
+ scaling_factor = 0.9
+ if text_width > (label_width * scaling_factor):
+ return int(initial_font_size * (label_width / text_width) * scaling_factor)
+ else:
+ return initial_font_size
+
+ def create_label(self, img, text, initial_font_size, is_x_label=True, max_font_size=70, min_font_size=10):
+ label_width = img.width if is_x_label else img.height
+
+ # Adjust font size
+ font_size = self.adjust_font_size(text, initial_font_size, label_width)
+ font_size = min(max_font_size, font_size) # Ensure font isn't too large
+ font_size = max(min_font_size, font_size) # Ensure font isn't too small
+
+ label_height = int(font_size * 1.5) if is_x_label else font_size
+
+ label_bg = Image.new('RGBA', (label_width, label_height), color=(255, 255, 255, 0))
+ d = ImageDraw.Draw(label_bg)
+
+ font = self.get_font(font_size)
+
+ # Check if text will fit, if not insert ellipsis and reduce text
+ if d.textsize(text, font=font)[0] > label_width:
+ while d.textsize(text+'...', font=font)[0] > label_width and len(text) > 0:
+ text = text[:-1]
+ text = text + '...'
+
+ # Compute text width and height for multi-line text
+ text_lines = text.split('\n')
+ text_widths, text_heights = zip(*[d.textsize(line, font=font) for line in text_lines])
+ max_text_width = max(text_widths)
+ total_text_height = sum(text_heights)
+
+ # Compute position for each line of text
+ lines_positions = []
+ current_y = 0
+ for line, line_width, line_height in zip(text_lines, text_widths, text_heights):
+ text_x = (label_width - line_width) // 2
+ text_y = current_y + (label_height - total_text_height) // 2
+ current_y += line_height
+ lines_positions.append((line, (text_x, text_y)))
+
+ # Draw each line of text
+ for line, (text_x, text_y) in lines_positions:
+ d.text((text_x, text_y), line, fill='black', font=font)
+
+ return label_bg
+
+
+ def sample_plot_image(self, plot_image_vars, samples, preview_latent, latents_plot, image_list, disable_noise, start_step, last_step, force_full_denoise):
+ model, clip, vae, positive, negative = None, None, None, None, None
+
+ if plot_image_vars["x_node_type"] == "loader" or plot_image_vars["y_node_type"] == "loader":
+ model, clip, vae = ttNcache.load_checkpoint(plot_image_vars['ckpt_name'])
+
+ if plot_image_vars['lora1_name'] != "None":
+ model, clip = ttNcache.load_lora(plot_image_vars['lora1_name'], model, clip, plot_image_vars['lora1_model_strength'], plot_image_vars['lora1_clip_strength'])
+
+ if plot_image_vars['lora2_name'] != "None":
+ model, clip = ttNcache.load_lora(plot_image_vars['lora2_name'], model, clip, plot_image_vars['lora2_model_strength'], plot_image_vars['lora2_clip_strength'])
+
+ if plot_image_vars['lora3_name'] != "None":
+ model, clip = ttNcache.load_lora(plot_image_vars['lora3_name'], model, clip, plot_image_vars['lora3_model_strength'], plot_image_vars['lora3_clip_strength'])
+
+ # Check for custom VAE
+ if plot_image_vars['vae_name'] not in ["Baked-VAE", "Baked VAE"]:
+ vae = ttNcache.load_vae(plot_image_vars['vae_name'])
+
+ # CLIP skip
+ if not clip:
+ raise Exception("No CLIP found")
+ clip = clip.clone()
+ clip.clip_layer(plot_image_vars['clip_skip'])
+
+ positive, positive_pooled = advanced_encode(clip, plot_image_vars['positive'], plot_image_vars['positive_token_normalization'], plot_image_vars['positive_weight_interpretation'], w_max=1.0, apply_to_pooled="enable")
+ positive = [[positive, {"pooled_output": positive_pooled}]]
+
+ negative, negative_pooled = advanced_encode(clip, plot_image_vars['negative'], plot_image_vars['negative_token_normalization'], plot_image_vars['negative_weight_interpretation'], w_max=1.0, apply_to_pooled="enable")
+ negative = [[negative, {"pooled_output": negative_pooled}]]
+
+ model = model if model is not None else plot_image_vars["model"]
+ clip = clip if clip is not None else plot_image_vars["clip"]
+ vae = vae if vae is not None else plot_image_vars["vae"]
+ positive = positive if positive is not None else plot_image_vars["positive_cond"]
+ negative = negative if negative is not None else plot_image_vars["negative_cond"]
+
+ seed = plot_image_vars["seed"]
+ steps = plot_image_vars["steps"]
+ cfg = plot_image_vars["cfg"]
+ sampler_name = plot_image_vars["sampler_name"]
+ scheduler = plot_image_vars["scheduler"]
+ denoise = plot_image_vars["denoise"]
+
+ if plot_image_vars["lora_name"] not in ('None', None):
+ model, clip = ttNcache.load_lora(plot_image_vars["lora_name"], model, clip, plot_image_vars["lora_model_strength"], plot_image_vars["lora_clip_strength"])
+
+ # Sample
+ samples = sampler.common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, samples, denoise=denoise, disable_noise=disable_noise, preview_latent=preview_latent, start_step=start_step, last_step=last_step, force_full_denoise=force_full_denoise)
+
+ # Decode images and store
+ latent = samples["samples"]
+
+ # Add the latent tensor to the tensors list
+ latents_plot.append(latent)
+
+ # Decode the image
+ image = vae.decode(latent).cpu()
+
+ if self.output_individuals in [True, "True"]:
+ ttN_save = ttNsave(self.my_unique_id, self.prompt, self.extra_pnginfo)
+ ttN_save.images(image, self.save_prefix, self.image_output, group_id=self.num)
+
+ # Convert the image from tensor to PIL Image and add it to the list
+ pil_image = ttNsampler.tensor2pil(image)
+ image_list.append(pil_image)
+
+ # Update max dimensions
+ self.max_width = max(self.max_width, pil_image.width)
+ self.max_height = max(self.max_height, pil_image.height)
+
+ # Return the touched variables
+ return image_list, self.max_width, self.max_height, latents_plot
+
+ # Process Functions
+ def validate_xy_plot(self):
+ if self.x_type == 'None' and self.y_type == 'None':
+ ttNl('No Valid Plot Types - Reverting to default sampling...').t(f'pipeKSampler[{self.my_unique_id}]').warn().p()
+ return False
+ else:
+ return True
+
+ def get_latent(self, samples):
+ # Extract the 'samples' tensor from the dictionary
+ latent_image_tensor = samples["samples"]
+
+ # Split the tensor into individual image tensors
+ image_tensors = torch.split(latent_image_tensor, 1, dim=0)
+
+ # Create a list of dictionaries containing the individual image tensors
+ latent_list = [{'samples': image} for image in image_tensors]
+
+ # Set latent only to the first latent of batch
+ if self.latent_id >= len(latent_list):
+ ttNl(f'The selected latent_id ({self.latent_id}) is out of range.').t(f'pipeKSampler[{self.my_unique_id}]').warn().p()
+ ttNl(f'Automatically setting the latent_id to the last image in the list (index: {len(latent_list) - 1}).').t(f'pipeKSampler[{self.my_unique_id}]').warn().p()
+
+ self.latent_id = len(latent_list) - 1
+
+ return latent_list[self.latent_id]
+
+ def get_labels_and_sample(self, plot_image_vars, latent_image, preview_latent, start_step, last_step, force_full_denoise, disable_noise):
+ for x_index, x_value in enumerate(self.x_values):
+ plot_image_vars, x_value_label = self.define_variable(plot_image_vars, self.x_type, x_value, x_index)
+ self.x_label = self.update_label(self.x_label, x_value_label, len(self.x_values))
+ if self.y_type != 'None':
+ for y_index, y_value in enumerate(self.y_values):
+ self.num += 1
+ plot_image_vars, y_value_label = self.define_variable(plot_image_vars, self.y_type, y_value, y_index)
+ self.y_label = self.update_label(self.y_label, y_value_label, len(self.y_values))
+
+ ttNl(f'{CC.GREY}X: {x_value_label}, Y: {y_value_label}').t(f'Plot Values {self.num}/{self.total} ->').p()
+ self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list, disable_noise, start_step, last_step, force_full_denoise)
+ else:
+ self.num += 1
+ ttNl(f'{CC.GREY}X: {x_value_label}').t(f'Plot Values {self.num}/{self.total} ->').p()
+ self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list, disable_noise, start_step, last_step, force_full_denoise)
+
+ # Rearrange latent array to match preview image grid
+ self.latents_plot = self.rearrange_tensors(self.latents_plot, self.num_cols, self.num_rows)
+
+ # Concatenate the tensors along the first dimension (dim=0)
+ self.latents_plot = torch.cat(self.latents_plot, dim=0)
+
+ return self.latents_plot
+
+ def plot_images_and_labels(self):
+ # Calculate the background dimensions
+ bg_width, bg_height, x_offset_initial, y_offset = self.calculate_background_dimensions()
+
+ # Create the white background image
+ background = Image.new('RGBA', (int(bg_width), int(bg_height)), color=(255, 255, 255, 255))
+
+ for row_index in range(self.num_rows):
+ x_offset = x_offset_initial
+
+ for col_index in range(self.num_cols):
+ index = col_index * self.num_rows + row_index
+ img = self.image_list[index]
+ background.paste(img, (x_offset, y_offset))
+
+ # Handle X label
+ if row_index == 0 and self.x_type != "None":
+ label_bg = self.create_label(img, self.x_label[col_index], int(48 * img.width / 512))
+ label_y = (y_offset - label_bg.height) // 2
+ background.alpha_composite(label_bg, (x_offset, label_y))
+
+ # Handle Y label
+ if col_index == 0 and self.y_type != "None":
+ label_bg = self.create_label(img, self.y_label[row_index], int(48 * img.height / 512), False)
+ label_bg = label_bg.rotate(90, expand=True)
+
+ label_x = (x_offset - label_bg.width) // 2
+ label_y = y_offset + (img.height - label_bg.height) // 2
+ background.alpha_composite(label_bg, (label_x, label_y))
+
+ x_offset += img.width + self.grid_spacing
+
+ y_offset += img.height + self.grid_spacing
+
+ return sampler.pil2tensor(background)
+
+class ttNsave:
+ def __init__(self, my_unique_id=0, prompt=None, extra_pnginfo=None, number_padding=5, overwrite_existing=False, output_dir=folder_paths.get_temp_directory()):
+ self.number_padding = int(number_padding) if number_padding not in [None, "None", 0] else None
+ self.overwrite_existing = overwrite_existing
+ self.my_unique_id = my_unique_id
+ self.prompt = prompt
+ self.extra_pnginfo = extra_pnginfo
+ self.type = 'temp'
+ self.output_dir = output_dir
+ if self.output_dir != folder_paths.get_temp_directory():
+ self.output_dir = self.folder_parser(self.output_dir, self.prompt, self.my_unique_id)
+ if not os.path.exists(self.output_dir):
+ self._create_directory(self.output_dir)
+
+ @staticmethod
+ def _create_directory(folder: str):
+ """Try to create the directory and log the status."""
+ ttNl(f"Folder {folder} does not exist. Attempting to create...").warn().p()
+ if not os.path.exists(folder):
+ try:
+ os.makedirs(folder)
+ ttNl(f"{folder} Created Successfully").success().p()
+ except OSError:
+ ttNl(f"Failed to create folder {folder}").error().p()
+ pass
+
+ @staticmethod
+ def _map_filename(filename: str, filename_prefix: str) -> Tuple[int, str, Optional[int]]:
+ """Utility function to map filename to its parts."""
+
+ # Get the prefix length and extract the prefix
+ prefix_len = len(os.path.basename(filename_prefix))
+ prefix = filename[:prefix_len]
+
+ # Search for the primary digits
+ digits = re.search(r'(\d+)', filename[prefix_len:])
+
+ # Search for the number in brackets after the primary digits
+ group_id = re.search(r'\((\d+)\)', filename[prefix_len:])
+
+ return (int(digits.group()) if digits else 0, prefix, int(group_id.group(1)) if group_id else 0)
+
+ @staticmethod
+ def _format_date(text: str, date: datetime.datetime) -> str:
+ """Format the date according to specific patterns."""
+ date_formats = {
+ 'd': lambda d: d.day,
+ 'dd': lambda d: '{:02d}'.format(d.day),
+ 'M': lambda d: d.month,
+ 'MM': lambda d: '{:02d}'.format(d.month),
+ 'h': lambda d: d.hour,
+ 'hh': lambda d: '{:02d}'.format(d.hour),
+ 'm': lambda d: d.minute,
+ 'mm': lambda d: '{:02d}'.format(d.minute),
+ 's': lambda d: d.second,
+ 'ss': lambda d: '{:02d}'.format(d.second),
+ 'y': lambda d: d.year,
+ 'yy': lambda d: str(d.year)[2:],
+ 'yyy': lambda d: str(d.year)[1:],
+ 'yyyy': lambda d: d.year,
+ }
+
+ # We need to sort the keys in reverse order to ensure we match the longest formats first
+ for format_str in sorted(date_formats.keys(), key=len, reverse=True):
+ if format_str in text:
+ text = text.replace(format_str, str(date_formats[format_str](date)))
+ return text
+
+ @staticmethod
+ def _gather_all_inputs(prompt: Dict[str, dict], unique_id: str, linkInput: str = '', collected_inputs: Optional[Dict[str, Union[str, List[str]]]] = None) -> Dict[str, Union[str, List[str]]]:
+ """Recursively gather all inputs from the prompt dictionary."""
+ if prompt == None:
+ return None
+
+ collected_inputs = collected_inputs or {}
+ prompt_inputs = prompt[str(unique_id)]["inputs"]
+
+ for p_input, p_input_value in prompt_inputs.items():
+ a_input = f"{linkInput}>{p_input}" if linkInput else p_input
+
+ if isinstance(p_input_value, list):
+ ttNsave._gather_all_inputs(prompt, p_input_value[0], a_input, collected_inputs)
+ else:
+ existing_value = collected_inputs.get(a_input)
+ if existing_value is None:
+ collected_inputs[a_input] = p_input_value
+ elif p_input_value not in existing_value:
+ collected_inputs[a_input] = existing_value + "; " + p_input_value
+
+ return collected_inputs
+
+ @staticmethod
+ def _get_filename_with_padding(output_dir, filename, number_padding, group_id, ext):
+ """Return filename with proper padding."""
+ try:
+ filtered = list(filter(lambda a: a[1] == filename, map(lambda x: ttNsave._map_filename(x, filename), os.listdir(output_dir))))
+ last = max(filtered)[0]
+
+ for f in filtered:
+ if f[0] == last:
+ if f[2] == 0 or f[2] == group_id:
+ last += 1
+ counter = last
+ except (ValueError, FileNotFoundError):
+ os.makedirs(output_dir, exist_ok=True)
+ counter = 1
+
+ if group_id == 0:
+ return f"{filename}.{ext}" if number_padding is None else f"{filename}_{counter:0{number_padding}}.{ext}"
+ else:
+ return f"{filename}_({group_id}).{ext}" if number_padding is None else f"{filename}_{counter:0{number_padding}}_({group_id}).{ext}"
+
+ @staticmethod
+ def filename_parser(output_dir: str, filename_prefix: str, prompt: Dict[str, dict], my_unique_id: str, number_padding: int, group_id: int, ext: str) -> str:
+ """Parse the filename using provided patterns and replace them with actual values."""
+ subfolder = os.path.dirname(os.path.normpath(filename_prefix))
+ filename = os.path.basename(os.path.normpath(filename_prefix))
+
+ filename = re.sub(r'%date:(.*?)%', lambda m: ttNsave._format_date(m.group(1), datetime.datetime.now()), filename_prefix)
+ all_inputs = ttNsave._gather_all_inputs(prompt, my_unique_id)
+
+ filename = re.sub(r'%(.*?)%', lambda m: str(all_inputs.get(m.group(1), '')), filename)
+ filename = re.sub(r'[/\\]+', '-', filename)
+
+ filename = ttNsave._get_filename_with_padding(output_dir, filename, number_padding, group_id, ext)
+
+ return filename, subfolder
+
+ @staticmethod
+ def folder_parser(output_dir: str, prompt: Dict[str, dict], my_unique_id: str):
+ output_dir = re.sub(r'%date:(.*?)%', lambda m: ttNsave._format_date(m.group(1), datetime.datetime.now()), output_dir)
+ all_inputs = ttNsave._gather_all_inputs(prompt, my_unique_id)
+
+ return re.sub(r'%(.*?)%', lambda m: str(all_inputs.get(m.group(1), '')), output_dir)
+
+ def images(self, images, filename_prefix, output_type, embed_workflow=True, ext="png", group_id=0):
+ FORMAT_MAP = {
+ "png": "PNG",
+ "jpg": "JPEG",
+ "jpeg": "JPEG",
+ "bmp": "BMP",
+ "tif": "TIFF",
+ "tiff": "TIFF"
+ }
+
+ if ext not in FORMAT_MAP:
+ raise ValueError(f"Unsupported file extension {ext}")
+
+ if output_type == "Hide":
+ return list()
+ if output_type in ("Save", "Hide/Save"):
+ output_dir = self.output_dir if self.output_dir != folder_paths.get_temp_directory() else folder_paths.get_output_directory()
+ self.type = "output"
+ if output_type == "Preview":
+ output_dir = self.output_dir
+ filename_prefix = 'ttNpreview'
+
+ results = list()
+ for image in images:
+ img = Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8))
+
+ filename = filename_prefix.replace("%width%", str(img.size[0])).replace("%height%", str(img.size[1]))
+
+ filename, subfolder = ttNsave.filename_parser(output_dir, filename, self.prompt, self.my_unique_id, self.number_padding, group_id, ext)
+
+ file_path = os.path.join(output_dir, filename)
+
+ if ext == "png" and embed_workflow in (True, "True"):
+ metadata = PngInfo()
+ if self.prompt is not None:
+ metadata.add_text("prompt", json.dumps(self.prompt))
+ if hasattr(self, 'extra_pnginfo') and self.extra_pnginfo is not None:
+ for key, value in self.extra_pnginfo.items():
+ metadata.add_text(key, json.dumps(value))
+ if self.overwrite_existing or not os.path.isfile(file_path):
+ img.save(file_path, pnginfo=metadata, format=FORMAT_MAP[ext])
+ else:
+ if self.overwrite_existing or not os.path.isfile(file_path):
+ img.save(file_path, format=FORMAT_MAP[ext])
+ else:
+ ttNl(f"File {file_path} already exists... Skipping").error().p()
+
+ results.append({
+ "filename": file_path,
+ "subfolder": subfolder,
+ "type": self.type
+ })
+
+ return results
+
+ def textfile(self, text, filename_prefix, output_type, group_id=0, ext='txt'):
+ if output_type == "Hide":
+ return []
+ if output_type in ("Save", "Hide/Save"):
+ output_dir = self.output_dir if self.output_dir != folder_paths.get_temp_directory() else folder_paths.get_output_directory()
+ if output_type == "Preview":
+ filename_prefix = 'ttNpreview'
+
+ filename = ttNsave.filename_parser(output_dir, filename_prefix, self.prompt, self.my_unique_id, self.number_padding, group_id, ext)
+
+ file_path = os.path.join(output_dir, filename)
+
+ if self.overwrite_existing or not os.path.isfile(file_path):
+ with open(file_path, 'w') as f:
+ f.write(text)
+ else:
+ ttNl(f"File {file_path} already exists... Skipping").error().p()
+
+ttNcache = ttNloader()
+sampler = ttNsampler()
+
+def nsp_parse(text, seed=0, noodle_key='__', nspterminology=None, pantry_path=None, title=None, my_unique_id=None):
+ if "__" not in text:
+ return text
+
+ if nspterminology is None:
+ # Fetch the NSP Pantry
+ if pantry_path is None:
+ pantry_path = os.path.join(ttNpaths.tinyterraNodes, 'nsp_pantry.json')
+ if not os.path.exists(pantry_path):
+ response = urlopen('https://raw.githubusercontent.com/WASasquatch/noodle-soup-prompts/main/nsp_pantry.json')
+ tmp_pantry = json.loads(response.read())
+ # Dump JSON locally
+ pantry_serialized = json.dumps(tmp_pantry, indent=4)
+ with open(pantry_path, "w") as f:
+ f.write(pantry_serialized)
+ del response, tmp_pantry
+
+ # Load local pantry
+ with open(pantry_path, 'r') as f:
+ nspterminology = json.load(f)
+
+ if seed > 0 or seed < 0:
+ random.seed(seed)
+
+ # Parse Text
+ new_text = text
+ for term in nspterminology:
+ # Target Noodle
+ tkey = f'{noodle_key}{term}{noodle_key}'
+ # How many occurrences?
+ tcount = new_text.count(tkey)
+
+ if tcount > 0:
+ nsp_parsed = True
+
+ # Apply random results for each noodle counted
+ for _ in range(tcount):
+ new_text = new_text.replace(
+ tkey, random.choice(nspterminology[term]), 1)
+ seed += 1
+ random.seed(seed)
+
+ ttNl(new_text).t(f'{title}[{my_unique_id}]').p()
+
+
+ return new_text
+
+#---------------------------------------------------------------ttN/pipe START----------------------------------------------------------------------#
+class ttN_TSC_pipeLoader:
+ version = '1.1.2'
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),
+ "config_name": (["Default",] + folder_paths.get_filename_list("configs"), {"default": "Default"} ),
+ "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
+ "clip_skip": ("INT", {"default": -1, "min": -24, "max": 0, "step": 1}),
+
+ "lora1_name": (["None"] + folder_paths.get_filename_list("loras"),),
+ "lora1_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "lora1_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+
+ "lora2_name": (["None"] + folder_paths.get_filename_list("loras"),),
+ "lora2_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "lora2_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+
+ "lora3_name": (["None"] + folder_paths.get_filename_list("loras"),),
+ "lora3_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "lora3_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+
+ "positive": ("STRING", {"default": "Positive","multiline": True}),
+ "positive_token_normalization": (["none", "mean", "length", "length+mean"],),
+ "positive_weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"],),
+
+ "negative": ("STRING", {"default": "Negative", "multiline": True}),
+ "negative_token_normalization": (["none", "mean", "length", "length+mean"],),
+ "negative_weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"],),
+
+ "empty_latent_width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "empty_latent_height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ },
+ "optional": {"model_override": ("MODEL",), "clip_override": ("CLIP",), "optional_lora_stack": ("LORA_STACK",),},
+ "hidden": {"prompt": "PROMPT", "ttNnodeVersion": ttN_TSC_pipeLoader.version}, "my_unique_id": "UNIQUE_ID",}
+
+ RETURN_TYPES = ("PIPE_LINE" ,"MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "INT",)
+ RETURN_NAMES = ("pipe","model", "positive", "negative", "latent", "vae", "clip", "seed",)
+
+ FUNCTION = "adv_pipeloader"
+ CATEGORY = "ttN/pipe"
+
+ def adv_pipeloader(self, ckpt_name, config_name, vae_name, clip_skip,
+ lora1_name, lora1_model_strength, lora1_clip_strength,
+ lora2_name, lora2_model_strength, lora2_clip_strength,
+ lora3_name, lora3_model_strength, lora3_clip_strength,
+ positive, positive_token_normalization, positive_weight_interpretation,
+ negative, negative_token_normalization, negative_weight_interpretation,
+ empty_latent_width, empty_latent_height, batch_size, seed, model_override=None, clip_override=None, optional_lora_stack=None, prompt=None, my_unique_id=None):
+
+ model: ModelPatcher | None = None
+ clip: CLIP | None = None
+ vae: VAE | None = None
+
+ # Create Empty Latent
+ latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8]).cpu()
+ samples = {"samples":latent}
+
+ # Clean models from loaded_objects
+ ttNcache.update_loaded_objects(prompt)
+
+ # Load models
+ model, clip, vae = ttNcache.load_checkpoint(ckpt_name, config_name)
+
+ if model_override is not None:
+ model = model_override
+
+ if clip_override is not None:
+ clip = clip_override
+
+ if optional_lora_stack is not None:
+ for lora in optional_lora_stack:
+ model, clip = ttNcache.load_lora(lora[0], model, clip, lora[1], lora[2])
+
+ if lora1_name != "None":
+ model, clip = ttNcache.load_lora(lora1_name, model, clip, lora1_model_strength, lora1_clip_strength)
+
+ if lora2_name != "None":
+ model, clip = ttNcache.load_lora(lora2_name, model, clip, lora2_model_strength, lora2_clip_strength)
+
+ if lora3_name != "None":
+ model, clip = ttNcache.load_lora(lora3_name, model, clip, lora3_model_strength, lora3_clip_strength)
+
+ # Check for custom VAE
+ if vae_name != "Baked VAE":
+ vae = ttNcache.load_vae(vae_name)
+
+ # CLIP skip
+ if not clip:
+ raise Exception("No CLIP found")
+
+ clipped = clip.clone()
+ if clip_skip != 0:
+ clipped.clip_layer(clip_skip)
+
+ positive = nsp_parse(positive, seed, title='pipeLoader Positive', my_unique_id=my_unique_id)
+
+ positive_embeddings_final, positive_pooled = advanced_encode(clipped, positive, positive_token_normalization, positive_weight_interpretation, w_max=1.0, apply_to_pooled='enable')
+ positive_embeddings_final = [[positive_embeddings_final, {"pooled_output": positive_pooled}]]
+
+ negative = nsp_parse(negative, seed, title='pipeLoader Negative', my_unique_id=my_unique_id)
+
+ negative_embeddings_final, negative_pooled = advanced_encode(clipped, negative, negative_token_normalization, negative_weight_interpretation, w_max=1.0, apply_to_pooled='enable')
+ negative_embeddings_final = [[negative_embeddings_final, {"pooled_output": negative_pooled}]]
+ image = ttNsampler.pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0)))
+
+
+ pipe = {"model": model,
+ "positive": positive_embeddings_final,
+ "negative": negative_embeddings_final,
+ "vae": vae,
+ "clip": clip,
+
+ "samples": samples,
+ "images": image,
+ "seed": seed,
+
+ "loader_settings": {"ckpt_name": ckpt_name,
+ "vae_name": vae_name,
+
+ "lora1_name": lora1_name,
+ "lora1_model_strength": lora1_model_strength,
+ "lora1_clip_strength": lora1_clip_strength,
+ "lora2_name": lora2_name,
+ "lora2_model_strength": lora2_model_strength,
+ "lora2_clip_strength": lora2_clip_strength,
+ "lora3_name": lora3_name,
+ "lora3_model_strength": lora3_model_strength,
+ "lora3_clip_strength": lora3_clip_strength,
+
+ "refiner_ckpt_name": None,
+ "refiner_vae_name": None,
+ "refiner_lora1_name": None,
+ "refiner_lora1_model_strength": None,
+ "refiner_lora1_clip_strength": None,
+ "refiner_lora2_name": None,
+ "refiner_lora2_model_strength": None,
+ "refiner_lora2_clip_strength": None,
+
+ "clip_skip": clip_skip,
+ "positive": positive,
+ "positive_l": None,
+ "positive_g": None,
+ "positive_token_normalization": positive_token_normalization,
+ "positive_weight_interpretation": positive_weight_interpretation,
+ "positive_balance": None,
+ "negative": negative,
+ "negative_l": None,
+ "negative_g": None,
+ "negative_token_normalization": negative_token_normalization,
+ "negative_weight_interpretation": negative_weight_interpretation,
+ "negative_balance": None,
+ "empty_latent_width": empty_latent_width,
+ "empty_latent_height": empty_latent_height,
+ "batch_size": batch_size,
+ "seed": seed,
+ "empty_samples": samples,}
+ }
+
+ return (pipe, model, positive_embeddings_final, negative_embeddings_final, samples, vae, clip, seed)
+
+class ttN_TSC_pipeKSampler:
+ version = '1.0.5'
+ upscale_methods = ["None", "nearest-exact", "bilinear", "area", "bicubic", "lanczos", "bislerp"]
+ crop_methods = ["disabled", "center"]
+
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required":
+ {"pipe": ("PIPE_LINE",),
+
+ "lora_name": (["None"] + folder_paths.get_filename_list("loras"),),
+ "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+
+ "upscale_method": (cls.upscale_methods,),
+ "factor": ("FLOAT", {"default": 2, "min": 0.0, "max": 10.0, "step": 0.25}),
+ "crop": (cls.crop_methods,),
+ "sampler_state": (["Sample", "Hold"], ),
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
+ "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
+ "image_output": (["Hide", "Preview", "Save", "Hide/Save"],),
+ "save_prefix": ("STRING", {"default": "ComfyUI"})
+ },
+ "optional":
+ {"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "optional_model": ("MODEL",),
+ "optional_positive": ("CONDITIONING",),
+ "optional_negative": ("CONDITIONING",),
+ "optional_latent": ("LATENT",),
+ "optional_vae": ("VAE",),
+ "optional_clip": ("CLIP",),
+ "xyPlot": ("XYPLOT",),
+ },
+ "hidden":
+ {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
+ "embeddingsList": (folder_paths.get_filename_list("embeddings"),),
+ "ttNnodeVersion": ttN_TSC_pipeKSampler.version},
+ }
+
+ RETURN_TYPES = ("PIPE_LINE", "MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "IMAGE", "INT",)
+ RETURN_NAMES = ("pipe", "model", "positive", "negative", "latent","vae", "clip", "image", "seed", )
+ OUTPUT_NODE = True
+ FUNCTION = "sample"
+ CATEGORY = "ttN/pipe"
+
+ def sample(self, pipe, lora_name, lora_model_strength, lora_clip_strength, sampler_state, steps, cfg, sampler_name, scheduler, image_output, save_prefix, denoise=1.0,
+ optional_model=None, optional_positive=None, optional_negative=None, optional_latent=None, optional_vae=None, optional_clip=None, seed=None, xyPlot=None, upscale_method=None, factor=None, crop=None, prompt=None, extra_pnginfo=None, my_unique_id=None, start_step=None, last_step=None, force_full_denoise=False, disable_noise=False):
+ # Clean Loader Models from Global
+ ttNcache.update_loaded_objects(prompt)
+
+ my_unique_id = int(my_unique_id)
+
+ ttN_save = ttNsave(my_unique_id, prompt, extra_pnginfo)
+
+ samp_model = optional_model if optional_model is not None else pipe["model"]
+ samp_positive = optional_positive if optional_positive is not None else pipe["positive"]
+ samp_negative = optional_negative if optional_negative is not None else pipe["negative"]
+ samp_samples = optional_latent if optional_latent is not None else pipe["samples"]
+ samp_vae = optional_vae if optional_vae is not None else pipe["vae"]
+ samp_clip = optional_clip if optional_clip is not None else pipe["clip"]
+
+ if seed in (None, 'undefined'):
+ samp_seed = pipe["seed"]
+ else:
+ samp_seed = seed
+
+ def process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, lora_name, lora_model_strength, lora_clip_strength,
+ steps, cfg, sampler_name, scheduler, denoise,
+ image_output, save_prefix, prompt, extra_pnginfo, my_unique_id, preview_latent, disable_noise=disable_noise):
+ # Load Lora
+ if lora_name not in (None, "None"):
+ samp_model, samp_clip = ttNcache.load_lora(lora_name, samp_model, samp_clip, lora_model_strength, lora_clip_strength)
+
+ # Upscale samples if enabled
+ samp_samples = sampler.handle_upscale(samp_samples, upscale_method, factor, crop)
+
+ samp_samples = sampler.common_ksampler(samp_model, samp_seed, steps, cfg, sampler_name, scheduler, samp_positive, samp_negative, samp_samples, denoise=denoise, preview_latent=preview_latent, start_step=start_step, last_step=last_step, force_full_denoise=force_full_denoise, disable_noise=disable_noise)
+
+
+ latent = samp_samples["samples"]
+ samp_images = samp_vae.decode(latent).cpu()
+
+ results = ttN_save.images(samp_images, save_prefix, image_output)
+
+ sampler.update_value_by_id("results", my_unique_id, results)
+
+ # Clean loaded_objects
+ ttNcache.update_loaded_objects(prompt)
+
+ new_pipe = {
+ "model": samp_model,
+ "positive": samp_positive,
+ "negative": samp_negative,
+ "vae": samp_vae,
+ "clip": samp_clip,
+
+ "samples": samp_samples,
+ "images": samp_images,
+ "seed": samp_seed,
+
+ "loader_settings": pipe["loader_settings"],
+ }
+
+ sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe)
+
+ del pipe
+
+ if image_output in ("Hide", "Hide/Save"):
+ return sampler.get_output(new_pipe)
+
+ return {"ui": {"images": results},
+ "result": sampler.get_output(new_pipe)}
+
+ def process_hold_state(pipe, image_output, my_unique_id):
+ last_pipe = sampler.init_state(my_unique_id, "pipe_line", pipe)
+
+ last_results = sampler.init_state(my_unique_id, "results", list())
+
+ if image_output in ("Hide", "Hide/Save"):
+ return sampler.get_output(last_pipe)
+
+ return {"ui": {"images": last_results}, "result": sampler.get_output(last_pipe)}
+
+ def process_xyPlot(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, lora_name, lora_model_strength, lora_clip_strength,
+ steps, cfg, sampler_name, scheduler, denoise,
+ image_output, save_prefix, prompt, extra_pnginfo, my_unique_id, preview_latent, xyPlot):
+
+ random.seed(seed)
+
+ sampleXYplot = ttNxyPlot(xyPlot, save_prefix, image_output, prompt, extra_pnginfo, my_unique_id)
+
+ if not sampleXYplot.validate_xy_plot():
+ return process_sample_state(pipe, lora_name, lora_model_strength, lora_clip_strength, steps, cfg, sampler_name, scheduler, denoise, image_output, save_prefix, prompt, extra_pnginfo, my_unique_id, preview_latent)
+
+ plot_image_vars = {
+ "x_node_type": sampleXYplot.x_node_type, "y_node_type": sampleXYplot.y_node_type,
+ "lora_name": lora_name, "lora_model_strength": lora_model_strength, "lora_clip_strength": lora_clip_strength,
+ "steps": steps, "cfg": cfg, "sampler_name": sampler_name, "scheduler": scheduler, "denoise": denoise, "seed": samp_seed,
+
+ "model": samp_model, "vae": samp_vae, "clip": samp_clip, "positive_cond": samp_positive, "negative_cond": samp_negative,
+
+ "ckpt_name": pipe['loader_settings']['ckpt_name'],
+ "vae_name": pipe['loader_settings']['vae_name'],
+ "clip_skip": pipe['loader_settings']['clip_skip'],
+ "lora1_name": pipe['loader_settings']['lora1_name'],
+ "lora1_model_strength": pipe['loader_settings']['lora1_model_strength'],
+ "lora1_clip_strength": pipe['loader_settings']['lora1_clip_strength'],
+ "lora2_name": pipe['loader_settings']['lora2_name'],
+ "lora2_model_strength": pipe['loader_settings']['lora2_model_strength'],
+ "lora2_clip_strength": pipe['loader_settings']['lora2_clip_strength'],
+ "lora3_name": pipe['loader_settings']['lora3_name'],
+ "lora3_model_strength": pipe['loader_settings']['lora3_model_strength'],
+ "lora3_clip_strength": pipe['loader_settings']['lora3_clip_strength'],
+ "positive": pipe['loader_settings']['positive'],
+ "positive_token_normalization": pipe['loader_settings']['positive_token_normalization'],
+ "positive_weight_interpretation": pipe['loader_settings']['positive_weight_interpretation'],
+ "negative": pipe['loader_settings']['negative'],
+ "negative_token_normalization": pipe['loader_settings']['negative_token_normalization'],
+ "negative_weight_interpretation": pipe['loader_settings']['negative_weight_interpretation'],
+ }
+
+ latent_image = sampleXYplot.get_latent(pipe["samples"])
+
+ latents_plot = sampleXYplot.get_labels_and_sample(plot_image_vars, latent_image, preview_latent, start_step, last_step, force_full_denoise, disable_noise)
+
+ samp_samples = {"samples": latents_plot}
+
+ images = sampleXYplot.plot_images_and_labels()
+
+ samp_images = images
+
+ results = ttN_save.images(images, save_prefix, image_output)
+
+ sampler.update_value_by_id("results", my_unique_id, results)
+
+ # Clean loaded_objects
+ ttNcache.update_loaded_objects(prompt)
+
+ new_pipe = {
+ "model": samp_model,
+ "positive": samp_positive,
+ "negative": samp_negative,
+ "vae": samp_vae,
+ "clip": samp_clip,
+
+ "samples": samp_samples,
+ "images": samp_images,
+ "seed": samp_seed,
+
+ "loader_settings": pipe["loader_settings"],
+ }
+
+ sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe)
+
+ del pipe
+
+ if image_output in ("Hide", "Hide/Save"):
+ return sampler.get_output(new_pipe)
+
+ return {"ui": {"images": results}, "result": sampler.get_output(new_pipe)}
+
+ preview_latent = True
+ if image_output in ("Hide", "Hide/Save"):
+ preview_latent = False
+
+ if sampler_state == "Sample" and xyPlot is None:
+ return process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, lora_name, lora_model_strength, lora_clip_strength,
+ steps, cfg, sampler_name, scheduler, denoise, image_output, save_prefix, prompt, extra_pnginfo, my_unique_id, preview_latent)
+
+ elif sampler_state == "Sample" and xyPlot is not None:
+ return process_xyPlot(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, lora_name, lora_model_strength, lora_clip_strength, steps, cfg, sampler_name, scheduler, denoise, image_output, save_prefix, prompt, extra_pnginfo, my_unique_id, preview_latent, xyPlot)
+
+ elif sampler_state == "Hold":
+ return process_hold_state(pipe, image_output, my_unique_id)
+
+class ttN_pipeKSamplerAdvanced:
+ version = '1.0.5'
+ upscale_methods = ["None", "nearest-exact", "bilinear", "area", "bicubic", "lanczos", "bislerp"]
+ crop_methods = ["disabled", "center"]
+
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required":
+ {"pipe": ("PIPE_LINE",),
+
+ "lora_name": (["None"] + folder_paths.get_filename_list("loras"),),
+ "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+
+ "upscale_method": (cls.upscale_methods,),
+ "factor": ("FLOAT", {"default": 2, "min": 0.0, "max": 10.0, "step": 0.25}),
+ "crop": (cls.crop_methods,),
+ "sampler_state": (["Sample", "Hold"], ),
+
+ "add_noise": (["enable", "disable"], ),
+
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
+
+ "start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
+ "end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
+ "return_with_leftover_noise": (["disable", "enable"], ),
+
+ "image_output": (["Hide", "Preview", "Save", "Hide/Save"],),
+ "save_prefix": ("STRING", {"default": "ComfyUI"})
+ },
+ "optional":
+ {"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "optional_model": ("MODEL",),
+ "optional_positive": ("CONDITIONING",),
+ "optional_negative": ("CONDITIONING",),
+ "optional_latent": ("LATENT",),
+ "optional_vae": ("VAE",),
+ "optional_clip": ("CLIP",),
+ "xyPlot": ("XYPLOT",),
+ },
+ "hidden":
+ {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
+ "embeddingsList": (folder_paths.get_filename_list("embeddings"),),
+ "ttNnodeVersion": ttN_pipeKSamplerAdvanced.version},
+ }
+
+ RETURN_TYPES = ("PIPE_LINE", "MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "IMAGE", "INT",)
+ RETURN_NAMES = ("pipe", "model", "positive", "negative", "latent","vae", "clip", "image", "seed", )
+ OUTPUT_NODE = True
+ FUNCTION = "sample"
+ CATEGORY = "ttN/pipe"
+
+ def sample(self, pipe,
+ lora_name, lora_model_strength, lora_clip_strength,
+ sampler_state, add_noise, steps, cfg, sampler_name, scheduler, image_output, save_prefix, denoise=1.0,
+ noise_seed=None, optional_model=None, optional_positive=None, optional_negative=None, optional_latent=None, optional_vae=None, optional_clip=None, xyPlot=None, upscale_method=None, factor=None, crop=None, prompt=None, extra_pnginfo=None, my_unique_id=None, start_at_step=None, end_at_step=None, return_with_leftover_noise=False):
+
+ force_full_denoise = True
+ if return_with_leftover_noise == "enable":
+ force_full_denoise = False
+
+ disable_noise = False
+ if add_noise == "disable":
+ disable_noise = True
+
+ return ttN_TSC_pipeKSampler.sample(self, pipe, lora_name, lora_model_strength, lora_clip_strength, sampler_state, steps, cfg, sampler_name, scheduler, image_output, save_prefix, denoise,
+ optional_model, optional_positive, optional_negative, optional_latent, optional_vae, optional_clip, noise_seed, xyPlot, upscale_method, factor, crop, prompt, extra_pnginfo, my_unique_id, start_at_step, end_at_step, force_full_denoise, disable_noise)
+
+class ttN_pipeLoaderSDXL:
+ version = '1.1.2'
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),
+ "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
+
+ "lora1_name": (["None"] + folder_paths.get_filename_list("loras"),),
+ "lora1_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "lora1_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+
+ "lora2_name": (["None"] + folder_paths.get_filename_list("loras"),),
+ "lora2_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "lora2_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+
+ "refiner_ckpt_name": (["None"] + folder_paths.get_filename_list("checkpoints"), ),
+ "refiner_vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
+
+ "refiner_lora1_name": (["None"] + folder_paths.get_filename_list("loras"),),
+ "refiner_lora1_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "refiner_lora1_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+
+ "refiner_lora2_name": (["None"] + folder_paths.get_filename_list("loras"),),
+ "refiner_lora2_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "refiner_lora2_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+
+ "clip_skip": ("INT", {"default": -2, "min": -24, "max": 0, "step": 1}),
+
+ "positive": ("STRING", {"default": "Positive","multiline": True}),
+ "positive_token_normalization": (["none", "mean", "length", "length+mean"],),
+ "positive_weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"],),
+
+ "negative": ("STRING", {"default": "Negative", "multiline": True}),
+ "negative_token_normalization": (["none", "mean", "length", "length+mean"],),
+ "negative_weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"],),
+
+ "empty_latent_width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "empty_latent_height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ },
+ "hidden": {"prompt": "PROMPT", "ttNnodeVersion": ttN_pipeLoaderSDXL.version}, "my_unique_id": "UNIQUE_ID"}
+
+ RETURN_TYPES = ("PIPE_LINE_SDXL" ,"MODEL", "CONDITIONING", "CONDITIONING", "VAE", "CLIP", "MODEL", "CONDITIONING", "CONDITIONING", "VAE", "CLIP", "LATENT", "INT",)
+ RETURN_NAMES = ("sdxl_pipe","model", "positive", "negative", "vae", "clip", "refiner_model", "refiner_positive", "refiner_negative", "refiner_vae", "refiner_clip", "latent", "seed",)
+
+ FUNCTION = "adv_pipeloader"
+ CATEGORY = "ttN/pipe"
+
+ def adv_pipeloader(self, ckpt_name, vae_name,
+ lora1_name, lora1_model_strength, lora1_clip_strength,
+ lora2_name, lora2_model_strength, lora2_clip_strength,
+ refiner_ckpt_name, refiner_vae_name,
+ refiner_lora1_name, refiner_lora1_model_strength, refiner_lora1_clip_strength,
+ refiner_lora2_name, refiner_lora2_model_strength, refiner_lora2_clip_strength,
+ clip_skip,
+ positive, positive_token_normalization, positive_weight_interpretation,
+ negative, negative_token_normalization, negative_weight_interpretation,
+ empty_latent_width, empty_latent_height, batch_size, seed, prompt=None, my_unique_id=None):
+
+ def SDXL_loader(ckpt_name, vae_name,
+ lora1_name, lora1_model_strength, lora1_clip_strength,
+ lora2_name, lora2_model_strength, lora2_clip_strength,
+ positive, positive_token_normalization, positive_weight_interpretation,
+ negative, negative_token_normalization, negative_weight_interpretation,):
+
+ model: ModelPatcher | None = None
+ clip: CLIP | None = None
+ vae: VAE | None = None
+
+ # Load models
+ model, clip, vae = ttNcache.load_checkpoint(ckpt_name)
+
+ if lora1_name != "None":
+ model, clip = ttNcache.load_lora(lora1_name, model, clip, lora1_model_strength, lora1_clip_strength)
+
+ if lora2_name != "None":
+ model, clip = ttNcache.load_lora(lora2_name, model, clip, lora2_model_strength, lora2_clip_strength)
+
+ # Check for custom VAE
+ if vae_name not in ["Baked VAE", "Baked-VAE"]:
+ vae = ttNcache.load_vae(vae_name)
+
+ # CLIP skip
+ if not clip:
+ raise Exception("No CLIP found")
+
+ clipped = clip.clone()
+ if clip_skip != 0:
+ clipped.clip_layer(clip_skip)
+
+ positive = nsp_parse(positive, seed, title="pipeLoaderSDXL positive", my_unique_id=my_unique_id)
+
+ positive_embeddings_final, positive_pooled = advanced_encode(clipped, positive, positive_token_normalization, positive_weight_interpretation, w_max=1.0, apply_to_pooled='enable')
+ positive_embeddings_final = [[positive_embeddings_final, {"pooled_output": positive_pooled}]]
+
+ negative = nsp_parse(negative, seed)
+
+ negative_embeddings_final, negative_pooled = advanced_encode(clipped, negative, negative_token_normalization, negative_weight_interpretation, w_max=1.0, apply_to_pooled='enable')
+ negative_embeddings_final = [[negative_embeddings_final, {"pooled_output": negative_pooled}]]
+
+ return model, positive_embeddings_final, negative_embeddings_final, vae, clip
+
+ # Create Empty Latent
+ latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8]).cpu()
+ samples = {"samples":latent}
+
+ model, positive_embeddings, negative_embeddings, vae, clip = SDXL_loader(ckpt_name, vae_name,
+ lora1_name, lora1_model_strength, lora1_clip_strength,
+ lora2_name, lora2_model_strength, lora2_clip_strength,
+ positive, positive_token_normalization, positive_weight_interpretation,
+ negative, negative_token_normalization, negative_weight_interpretation)
+
+ if refiner_ckpt_name != "None":
+ refiner_model, refiner_positive_embeddings, refiner_negative_embeddings, refiner_vae, refiner_clip = SDXL_loader(refiner_ckpt_name, refiner_vae_name,
+ refiner_lora1_name, refiner_lora1_model_strength, refiner_lora1_clip_strength,
+ refiner_lora2_name, refiner_lora2_model_strength, refiner_lora2_clip_strength,
+ positive, positive_token_normalization, positive_weight_interpretation,
+ negative, negative_token_normalization, negative_weight_interpretation)
+ else:
+ refiner_model, refiner_positive_embeddings, refiner_negative_embeddings, refiner_vae, refiner_clip = None, None, None, None, None
+
+ # Clean models from loaded_objects
+ ttNcache.update_loaded_objects(prompt)
+
+ image = ttNsampler.pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0)))
+
+ pipe = {"model": model,
+ "positive": positive_embeddings,
+ "negative": negative_embeddings,
+ "vae": vae,
+ "clip": clip,
+
+ "refiner_model": refiner_model,
+ "refiner_positive": refiner_positive_embeddings,
+ "refiner_negative": refiner_negative_embeddings,
+ "refiner_vae": refiner_vae,
+ "refiner_clip": refiner_clip,
+
+ "samples": samples,
+ "images": image,
+ "seed": seed,
+
+ "loader_settings": {"ckpt_name": ckpt_name,
+ "vae_name": vae_name,
+
+ "lora1_name": lora1_name,
+ "lora1_model_strength": lora1_model_strength,
+ "lora1_clip_strength": lora1_clip_strength,
+ "lora2_name": lora2_name,
+ "lora2_model_strength": lora2_model_strength,
+ "lora2_clip_strength": lora2_clip_strength,
+ "lora3_name": None,
+ "lora3_model_strength": None,
+ "lora3_clip_strength": None,
+
+ "refiner_ckpt_name": refiner_ckpt_name,
+ "refiner_vae_name": refiner_vae_name,
+ "refiner_lora1_name": refiner_lora1_name,
+ "refiner_lora1_model_strength": refiner_lora1_model_strength,
+ "refiner_lora1_clip_strength": refiner_lora1_clip_strength,
+ "refiner_lora2_name": refiner_lora2_name,
+ "refiner_lora2_model_strength": refiner_lora2_model_strength,
+ "refiner_lora2_clip_strength": refiner_lora2_clip_strength,
+
+ "clip_skip": clip_skip,
+ "positive_balance": None,
+ "positive": positive,
+ "positive_l": None,
+ "positive_g": None,
+ "positive_token_normalization": positive_token_normalization,
+ "positive_weight_interpretation": positive_weight_interpretation,
+ "negative_balance": None,
+ "negative": negative,
+ "negative_l": None,
+ "negative_g": None,
+ "negative_token_normalization": negative_token_normalization,
+ "negative_weight_interpretation": negative_weight_interpretation,
+ "empty_latent_width": empty_latent_width,
+ "empty_latent_height": empty_latent_height,
+ "batch_size": batch_size,
+ "seed": seed,
+ "empty_samples": samples,}
+ }
+
+ return (pipe, model, positive_embeddings, negative_embeddings, vae, clip, refiner_model, refiner_positive_embeddings, refiner_negative_embeddings, refiner_vae, refiner_clip, samples, seed)
+
+class ttN_pipeKSamplerSDXL:
+ version = '1.0.2'
+ upscale_methods = ["None", "nearest-exact", "bilinear", "area", "bicubic", "lanczos", "bislerp"]
+ crop_methods = ["disabled", "center"]
+
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required":
+ {"sdxl_pipe": ("PIPE_LINE_SDXL",),
+
+ "upscale_method": (cls.upscale_methods,),
+ "factor": ("FLOAT", {"default": 2, "min": 0.0, "max": 10.0, "step": 0.25}),
+ "crop": (cls.crop_methods,),
+ "sampler_state": (["Sample", "Hold"], ),
+
+ "base_steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "refiner_steps": ("INT", {"default": 20, "min": 0, "max": 10000}),
+ "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
+ "image_output": (["Hide", "Preview", "Save", "Hide/Save"],),
+ "save_prefix": ("STRING", {"default": "ComfyUI"})
+ },
+ "optional":
+ {"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "optional_model": ("MODEL",),
+ "optional_positive": ("CONDITIONING",),
+ "optional_negative": ("CONDITIONING",),
+ "optional_vae": ("VAE",),
+ "optional_refiner_model": ("MODEL",),
+ "optional_refiner_positive": ("CONDITIONING",),
+ "optional_refiner_negative": ("CONDITIONING",),
+ "optional_refiner_vae": ("VAE",),
+ "optional_latent": ("LATENT",),
+ "optional_clip": ("CLIP",),
+ #"xyPlot": ("XYPLOT",),
+ },
+ "hidden":
+ {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
+ "embeddingsList": (folder_paths.get_filename_list("embeddings"),),
+ "ttNnodeVersion": ttN_pipeKSamplerSDXL.version
+ },
+ }
+
+ RETURN_TYPES = ("PIPE_LINE_SDXL", "MODEL", "CONDITIONING", "CONDITIONING", "VAE", "MODEL", "CONDITIONING", "CONDITIONING", "VAE", "LATENT", "CLIP", "IMAGE", "INT",)
+ RETURN_NAMES = ("sdxl_pipe", "model", "positive", "negative" ,"vae", "refiner_model", "refiner_positive", "refiner_negative" ,"refiner_vae", "latent", "clip", "image", "seed", )
+ OUTPUT_NODE = True
+ FUNCTION = "sample"
+ CATEGORY = "ttN/pipe"
+
+ def sample(self, sdxl_pipe, sampler_state,
+ base_steps, refiner_steps, cfg, sampler_name, scheduler, image_output, save_prefix, denoise=1.0,
+ optional_model=None, optional_positive=None, optional_negative=None, optional_latent=None, optional_vae=None, optional_clip=None,
+ optional_refiner_model=None, optional_refiner_positive=None, optional_refiner_negative=None, optional_refiner_vae=None,
+ seed=None, xyPlot=None, upscale_method=None, factor=None, crop=None, prompt=None, extra_pnginfo=None, my_unique_id=None,
+ start_step=None, last_step=None, force_full_denoise=False, disable_noise=False):
+
+ sdxl_pipe = {**sdxl_pipe}
+
+ # Clean Loader Models from Global
+ ttNcache.update_loaded_objects(prompt)
+
+ my_unique_id = int(my_unique_id)
+
+ ttN_save = ttNsave(my_unique_id, prompt, extra_pnginfo)
+
+ sdxl_samples = optional_latent if optional_latent is not None else sdxl_pipe["samples"]
+
+ sdxl_model = optional_model if optional_model is not None else sdxl_pipe["model"]
+ sdxl_positive = optional_positive if optional_positive is not None else sdxl_pipe["positive"]
+ sdxl_negative = optional_negative if optional_negative is not None else sdxl_pipe["negative"]
+ sdxl_vae = optional_vae if optional_vae is not None else sdxl_pipe["vae"]
+ sdxl_clip = optional_clip if optional_clip is not None else sdxl_pipe["clip"]
+ sdxl_refiner_model = optional_refiner_model if optional_refiner_model is not None else sdxl_pipe["refiner_model"]
+ sdxl_refiner_positive = optional_refiner_positive if optional_refiner_positive is not None else sdxl_pipe["refiner_positive"]
+ sdxl_refiner_negative = optional_refiner_negative if optional_refiner_negative is not None else sdxl_pipe["refiner_negative"]
+ sdxl_refiner_vae = optional_refiner_vae if optional_refiner_vae is not None else sdxl_pipe["refiner_vae"]
+ sdxl_refiner_clip = sdxl_pipe["refiner_clip"]
+
+ if seed in (None, 'undefined'):
+ sdxl_seed = sdxl_pipe["seed"]
+ else:
+ sdxl_seed = seed
+
+ def process_sample_state(sdxl_pipe, sdxl_samples, sdxl_model, sdxl_positive, sdxl_negative, sdxl_vae, sdxl_clip, sdxl_seed,
+ sdxl_refiner_model, sdxl_refiner_positive, sdxl_refiner_negative, sdxl_refiner_vae, sdxl_refiner_clip,
+ base_steps, refiner_steps, cfg, sampler_name, scheduler, denoise,
+ image_output, save_prefix, prompt, my_unique_id, preview_latent, disable_noise=disable_noise):
+
+ total_steps = base_steps + refiner_steps
+
+ # Upscale samples if enabled
+ sdxl_samples = sampler.handle_upscale(sdxl_samples, upscale_method, factor, crop)
+
+
+ if (refiner_steps > 0) and (sdxl_refiner_model not in [None, "None"]):
+ # Base Sample
+ sdxl_samples = sampler.common_ksampler(sdxl_model, sdxl_seed, total_steps, cfg, sampler_name, scheduler, sdxl_positive, sdxl_negative, sdxl_samples,
+ denoise=denoise, preview_latent=preview_latent, start_step=0, last_step=base_steps, force_full_denoise=force_full_denoise, disable_noise=disable_noise)
+
+ # Refiner Sample
+ sdxl_samples = sampler.common_ksampler(sdxl_refiner_model, sdxl_seed, total_steps, cfg, sampler_name, scheduler, sdxl_refiner_positive, sdxl_refiner_negative, sdxl_samples,
+ denoise=denoise, preview_latent=preview_latent, start_step=base_steps, last_step=10000, force_full_denoise=True, disable_noise=True)
+
+ latent = sdxl_samples["samples"]
+ sdxl_images = sdxl_refiner_vae.decode(latent).cpu()
+ del latent
+ else:
+ sdxl_samples = sampler.common_ksampler(sdxl_model, sdxl_seed, base_steps, cfg, sampler_name, scheduler, sdxl_positive, sdxl_negative, sdxl_samples,
+ denoise=denoise, preview_latent=preview_latent, start_step=0, last_step=base_steps, force_full_denoise=True, disable_noise=disable_noise)
+
+ latent = sdxl_samples["samples"]
+ sdxl_images = sdxl_vae.decode(latent).cpu()
+ del latent
+
+ results = ttN_save.images(sdxl_images, save_prefix, image_output)
+
+ sampler.update_value_by_id("results", my_unique_id, results)
+
+ # Clean loaded_objects
+ ttNcache.update_loaded_objects(prompt)
+
+ new_sdxl_pipe = {"model": sdxl_model,
+ "positive": sdxl_positive,
+ "negative": sdxl_negative,
+ "vae": sdxl_vae,
+ "clip": sdxl_clip,
+
+ "refiner_model": sdxl_refiner_model,
+ "refiner_positive": sdxl_refiner_positive,
+ "refiner_negative": sdxl_refiner_negative,
+ "refiner_vae": sdxl_refiner_vae,
+ "refiner_clip": sdxl_refiner_clip,
+
+ "samples": sdxl_samples,
+ "images": sdxl_images,
+ "seed": sdxl_seed,
+
+ "loader_settings": sdxl_pipe["loader_settings"],
+ }
+
+ del sdxl_pipe
+
+ sampler.update_value_by_id("pipe_line", my_unique_id, new_sdxl_pipe)
+
+ if image_output in ("Hide", "Hide/Save"):
+ return sampler.get_output_sdxl(new_sdxl_pipe)
+
+ return {"ui": {"images": results},
+ "result": sampler.get_output_sdxl(new_sdxl_pipe)}
+
+ def process_hold_state(sdxl_pipe, image_output, my_unique_id):
+ ttNl('Held').t(f'pipeKSamplerSDXL[{my_unique_id}]').p()
+
+ last_pipe = sampler.init_state(my_unique_id, "pipe_line", sdxl_pipe)
+
+ last_results = sampler.init_state(my_unique_id, "results", list())
+
+ if image_output in ("Hide", "Hide/Save"):
+ return sampler.get_output_sdxl(last_pipe)
+
+ return {"ui": {"images": last_results}, "result": sampler.get_output_sdxl(last_pipe)}
+
+ preview_latent = True
+ if image_output in ("Hide", "Hide/Save"):
+ preview_latent = False
+
+ if sampler_state == "Sample" and xyPlot is None:
+ return process_sample_state(sdxl_pipe, sdxl_samples, sdxl_model, sdxl_positive, sdxl_negative, sdxl_vae, sdxl_clip, sdxl_seed,
+ sdxl_refiner_model, sdxl_refiner_positive, sdxl_refiner_negative, sdxl_refiner_vae, sdxl_refiner_clip, base_steps, refiner_steps, cfg, sampler_name, scheduler, denoise, image_output, save_prefix, prompt, my_unique_id, preview_latent)
+
+ #elif sampler_state == "Sample" and xyPlot is not None:
+ # return process_xyPlot(sdxl_pipe, lora_name, lora_model_strength, lora_clip_strength, steps, cfg, sampler_name, scheduler, denoise, image_output, save_prefix, prompt, extra_pnginfo, my_unique_id, preview_latent, xyPlot)
+
+ elif sampler_state == "Hold":
+ return process_hold_state(sdxl_pipe, image_output, my_unique_id)
+
+class ttN_pipe_IN:
+ version = '1.1.0'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "model": ("MODEL",),
+ "pos": ("CONDITIONING",),
+ "neg": ("CONDITIONING",),
+ "latent": ("LATENT",),
+ "vae": ("VAE",),
+ "clip": ("CLIP",),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ },"optional": {
+ "image": ("IMAGE",),
+ },
+ "hidden": {"ttNnodeVersion": ttN_pipe_IN.version},
+ }
+
+ RETURN_TYPES = ("PIPE_LINE", )
+ RETURN_NAMES = ("pipe", )
+ FUNCTION = "flush"
+
+ CATEGORY = "ttN/legacy"
+
+ def flush(self, model, pos=0, neg=0, latent=0, vae=0, clip=0, image=0, seed=0):
+ pipe = {"model": model,
+ "positive": pos,
+ "negative": neg,
+ "vae": vae,
+ "clip": clip,
+
+ "refiner_model": None,
+ "refiner_positive": None,
+ "refiner_negative": None,
+ "refiner_vae": None,
+ "refiner_clip": None,
+
+ "samples": latent,
+ "images": image,
+ "seed": seed,
+
+ "loader_settings": {}
+ }
+ return (pipe, )
+
+class ttN_pipe_OUT:
+ version = '1.1.0'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "pipe": ("PIPE_LINE",),
+ },
+ "hidden": {"ttNnodeVersion": ttN_pipe_OUT.version},
+ }
+
+ RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "IMAGE", "INT", "PIPE_LINE",)
+ RETURN_NAMES = ("model", "pos", "neg", "latent", "vae", "clip", "image", "seed", "pipe")
+ FUNCTION = "flush"
+
+ CATEGORY = "ttN/legacy"
+
+ def flush(self, pipe):
+ model = pipe.get("model")
+ pos = pipe.get("positive")
+ neg = pipe.get("negative")
+ latent = pipe.get("samples")
+ vae = pipe.get("vae")
+ clip = pipe.get("clip")
+ image = pipe.get("images")
+ seed = pipe.get("seed")
+
+ return model, pos, neg, latent, vae, clip, image, seed, pipe
+
+class ttN_pipe_EDIT:
+ version = '1.1.1'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {},
+ "optional": {
+ "pipe": ("PIPE_LINE",),
+ "model": ("MODEL",),
+ "pos": ("CONDITIONING",),
+ "neg": ("CONDITIONING",),
+ "latent": ("LATENT",),
+ "vae": ("VAE",),
+ "clip": ("CLIP",),
+ "image": ("IMAGE",),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "forceInput": True}),
+ },
+ "hidden": {"ttNnodeVersion": ttN_pipe_EDIT.version, "my_unique_id": "UNIQUE_ID"},
+ }
+
+ RETURN_TYPES = ("PIPE_LINE", "MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "IMAGE", "INT")
+ RETURN_NAMES = ("pipe", "model", "pos", "neg", "latent", "vae", "clip", "image", "seed")
+ FUNCTION = "flush"
+
+ CATEGORY = "ttN/pipe"
+
+ def flush(self, pipe=None, model=None, pos=None, neg=None, latent=None, vae=None, clip=None, image=None, seed=None, my_unique_id=None):
+
+ model = model or pipe.get("model")
+ if model is None:
+ ttNl("Model missing from pipeLine").t(f'pipeEdit[{my_unique_id}]').warn().p()
+ pos = pos or pipe.get("positive")
+ if pos is None:
+ ttNl("Positive conditioning missing from pipeLine").t(f'pipeEdit[{my_unique_id}]').warn().p()
+ neg = neg or pipe.get("negative")
+ if neg is None:
+ ttNl("Negative conditioning missing from pipeLine").t(f'pipeEdit[{my_unique_id}]').warn().p()
+ samples = latent or pipe.get("samples")
+ if samples is None:
+ ttNl("Latent missing from pipeLine").t(f'pipeEdit[{my_unique_id}]').warn().p()
+ vae = vae or pipe.get("vae")
+ if vae is None:
+ ttNl("VAE missing from pipeLine").t(f'pipeEdit[{my_unique_id}]').warn().p()
+ clip = clip or pipe.get("clip")
+ if clip is None:
+ ttNl("Clip missing from pipeLine").t(f'pipeEdit[{my_unique_id}]').warn().p()
+ image = image or pipe.get("images")
+ if image is None:
+ ttNl("Image missing from pipeLine").t(f'pipeEdit[{my_unique_id}]').warn().p()
+ seed = seed or pipe.get("seed")
+ if seed is None:
+ ttNl("Seed missing from pipeLine").t(f'pipeEdit[{my_unique_id}]').warn().p()
+
+ new_pipe = {
+ "model": model,
+ "positive": pos,
+ "negative": neg,
+ "vae": vae,
+ "clip": clip,
+
+ "samples": samples,
+ "images": image,
+ "seed": seed,
+
+ "loader_settings": pipe["loader_settings"],
+ }
+ del pipe
+
+ return (new_pipe, )
+
+class ttN_pipe_2BASIC:
+ version = '1.1.0'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "pipe": ("PIPE_LINE",),
+ },
+ "hidden": {"ttNnodeVersion": ttN_pipe_2BASIC.version},
+ }
+
+ RETURN_TYPES = ("BASIC_PIPE", "PIPE_LINE",)
+ RETURN_NAMES = ("basic_pipe", "pipe",)
+ FUNCTION = "flush"
+
+ CATEGORY = "ttN/pipe"
+
+ def flush(self, pipe):
+ basic_pipe = (pipe.get('model'), pipe.get('clip'), pipe.get('vae'), pipe.get('positive'), pipe.get('negative'))
+ return (basic_pipe, pipe, )
+
+class ttN_pipe_2DETAILER:
+ version = '1.2.0'
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"pipe": ("PIPE_LINE",),
+ "bbox_detector": ("BBOX_DETECTOR", ),
+ "wildcard": ("STRING", {"multiline": True, "placeholder": "wildcard spec: if kept empty, this option will be ignored"}),
+ },
+ "optional": {"sam_model_opt": ("SAM_MODEL", ),
+ "segm_detector_opt": ("SEGM_DETECTOR",),
+ "detailer_hook": ("DETAILER_HOOK",),
+ },
+ "hidden": {"ttNnodeVersion": ttN_pipe_2DETAILER.version},
+ }
+
+ RETURN_TYPES = ("DETAILER_PIPE", "PIPE_LINE" )
+ RETURN_NAMES = ("detailer_pipe", "pipe")
+ FUNCTION = "flush"
+
+ CATEGORY = "ttN/pipe"
+
+ def flush(self, pipe, bbox_detector, wildcard, sam_model_opt=None, segm_detector_opt=None, detailer_hook=None):
+ detailer_pipe = (pipe.get('model'), pipe.get('clip'), pipe.get('vae'), pipe.get('positive'), pipe.get('negative'), wildcard,
+ bbox_detector, segm_detector_opt, sam_model_opt, detailer_hook, None, None, None, None)
+ return (detailer_pipe, pipe, )
+
+class ttN_XYPlot:
+ version = '1.2.0'
+ lora_list = ["None"] + folder_paths.get_filename_list("loras")
+ lora_strengths = {"min": -4.0, "max": 4.0, "step": 0.01}
+ token_normalization = ["none", "mean", "length", "length+mean"]
+ weight_interpretation = ["comfy", "A1111", "compel", "comfy++"]
+
+ loader_dict = {
+ "ckpt_name": folder_paths.get_filename_list("checkpoints"),
+ "vae_name": ["Baked-VAE"] + folder_paths.get_filename_list("vae"),
+ "clip_skip": {"min": -24, "max": -1, "step": 1},
+ "lora1_name": lora_list,
+ "lora1_model_strength": lora_strengths,
+ "lora1_clip_strength": lora_strengths,
+ "lora1_model&clip_strength": lora_strengths,
+ "lora2_name": lora_list,
+ "lora2_model_strength": lora_strengths,
+ "lora2_clip_strength": lora_strengths,
+ "lora2_model&clip_strength": lora_strengths,
+ "lora3_name": lora_list,
+ "lora3_model_strength": lora_strengths,
+ "lora3_clip_strength": lora_strengths,
+ "lora3_model&clip_strength": lora_strengths,
+ "positive": [],
+ "positive_token_normalization": token_normalization,
+ "positive_weight_interpretation": weight_interpretation,
+ "negative": [],
+ "negative_token_normalization": token_normalization,
+ "negative_weight_interpretation": weight_interpretation,
+ }
+
+ sampler_dict = {
+ "lora_name": lora_list,
+ "lora_model_strength": lora_strengths,
+ "lora_clip_strength": lora_strengths,
+ "lora_model&clip_strength": lora_strengths,
+ "steps": {"min": 1, "max": 100, "step": 1},
+ "cfg": {"min": 0.0, "max": 100.0, "step": 1.0},
+ "sampler_name": comfy.samplers.KSampler.SAMPLERS,
+ "scheduler": comfy.samplers.KSampler.SCHEDULERS,
+ "denoise": {"min": 0.0, "max": 1.0, "step": 0.01},
+ "seed": ['increment', 'decrement', 'randomize'],
+ }
+
+ plot_dict = {**sampler_dict, **loader_dict}
+
+ plot_values = ["None",]
+ plot_values.append("---------------------")
+ for k in sampler_dict:
+ plot_values.append(f'sampler: {k}')
+ plot_values.append("---------------------")
+ for k in loader_dict:
+ plot_values.append(f'loader: {k}')
+
+ def __init__(self):
+ pass
+
+ rejected = ["None", "---------------------"]
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ #"info": ("INFO", {"default": "Any values not set by xyplot will be taken from the KSampler or connected pipeLoader", "multiline": True}),
+ "grid_spacing": ("INT",{"min": 0, "max": 500, "step": 5, "default": 0,}),
+ "latent_id": ("INT",{"min": 0, "max": 100, "step": 1, "default": 0, }),
+ "output_individuals": (["False", "True"],{"default": "False"}),
+ "flip_xy": (["False", "True"],{"default": "False"}),
+ "x_axis": (ttN_XYPlot.plot_values, {"default": 'None'}),
+ "x_values": ("STRING",{"default": '', "multiline": True, "placeholder": 'insert values seperated by "; "'}),
+ "y_axis": (ttN_XYPlot.plot_values, {"default": 'None'}),
+ "y_values": ("STRING",{"default": '', "multiline": True, "placeholder": 'insert values seperated by "; "'}),
+ },
+ "hidden": {
+ "plot_dict": (ttN_XYPlot.plot_dict,),
+ "ttNnodeVersion": ttN_XYPlot.version,
+ },
+ }
+
+ RETURN_TYPES = ("XYPLOT", )
+ RETURN_NAMES = ("xyPlot", )
+ FUNCTION = "plot"
+
+ CATEGORY = "ttN/pipe"
+
+ def plot(self, grid_spacing, latent_id, output_individuals, flip_xy, x_axis, x_values, y_axis, y_values):
+ def clean_values(values):
+ original_values = values.split("; ")
+ cleaned_values = []
+
+ for value in original_values:
+ # Strip the semi-colon
+ cleaned_value = value.strip(';').strip()
+
+ if cleaned_value == "":
+ continue
+
+ # Try to convert the cleaned_value back to int or float if possible
+ try:
+ cleaned_value = int(cleaned_value)
+ except ValueError:
+ try:
+ cleaned_value = float(cleaned_value)
+ except ValueError:
+ pass
+
+ # Append the cleaned_value to the list
+ cleaned_values.append(cleaned_value)
+
+ return cleaned_values
+
+ if x_axis in self.rejected:
+ x_axis = "None"
+ x_values = []
+ else:
+ x_values = clean_values(x_values)
+
+ if y_axis in self.rejected:
+ y_axis = "None"
+ y_values = []
+ else:
+ y_values = clean_values(y_values)
+
+ if flip_xy == "True":
+ x_axis, y_axis = y_axis, x_axis
+ x_values, y_values = y_values, x_values
+
+ xy_plot = {"x_axis": x_axis,
+ "x_vals": x_values,
+ "y_axis": y_axis,
+ "y_vals": y_values,
+ "grid_spacing": grid_spacing,
+ "latent_id": latent_id,
+ "output_individuals": output_individuals}
+
+ return (xy_plot, )
+
+class ttN_pipeEncodeConcat:
+ version = '1.0.2'
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "pipe": ("PIPE_LINE",),
+ "toggle": ([True, False],),
+ },
+ "optional": {
+ "positive": ("STRING", {"default": "Positive","multiline": True}),
+ "positive_token_normalization": (["none", "mean", "length", "length+mean"],),
+ "positive_weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"],),
+ "negative": ("STRING", {"default": "Negative","multiline": True}),
+ "negative_token_normalization": (["none", "mean", "length", "length+mean"],),
+ "negative_weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"],),
+ "optional_positive_from": ("CONDITIONING",),
+ "optional_negative_from": ("CONDITIONING",),
+ "optional_clip": ("CLIP",),
+ },
+ "hidden": {
+ "ttNnodeVersion": ttN_pipeEncodeConcat.version, "my_unique_id": "UNIQUE_ID"
+ },
+ }
+
+ OUTPUT_NODE = True
+ RETURN_TYPES = ("PIPE_LINE", "CONDITIONING", "CONDITIONING", "CLIP")
+ RETURN_NAMES = ("pipe", "positive", "negative", "clip")
+ FUNCTION = "concat"
+
+ CATEGORY = "ttN/pipe"
+
+ def concat(self, toggle, positive_token_normalization, positive_weight_interpretation,
+ negative_token_normalization, negative_weight_interpretation,
+ pipe=None, positive='', negative='', seed=None, my_unique_id=None, optional_positive_from=None, optional_negative_from=None, optional_clip=None):
+
+ if toggle == False:
+ return (pipe, pipe["positive"], pipe["negative"], pipe["clip"])
+
+ positive_from = optional_positive_from if optional_positive_from is not None else pipe["positive"]
+ negative_from = optional_negative_from if optional_negative_from is not None else pipe["negative"]
+ samp_clip = optional_clip if optional_clip is not None else pipe["clip"]
+
+ new_text = ''
+
+ def enConcatConditioning(text, token_normalization, weight_interpretation, conditioning_from, new_text):
+ out = []
+ if "__" in text:
+ text = nsp_parse(text, pipe["seed"], title="encodeConcat", my_unique_id=my_unique_id)
+ new_text += text
+
+ conditioning_to, pooled = advanced_encode(samp_clip, text, token_normalization, weight_interpretation, w_max=1.0, apply_to_pooled='enable')
+ conditioning_to = [[conditioning_to, {"pooled_output": pooled}]]
+
+ if len(conditioning_from) > 1:
+ ttNl.warn("encode and concat conditioning_from contains more than 1 cond, only the first one will actually be applied to conditioning_to")
+
+ cond_from = conditioning_from[0][0]
+
+ for i in range(len(conditioning_to)):
+ t1 = conditioning_to[i][0]
+ tw = torch.cat((t1, cond_from),1)
+ n = [tw, conditioning_to[i][1].copy()]
+ out.append(n)
+
+ return out
+
+ if positive != '':
+ pos = enConcatConditioning(positive, positive_token_normalization, positive_weight_interpretation, positive_from, new_text)
+ if negative != '':
+ neg = enConcatConditioning(negative, negative_token_normalization, negative_weight_interpretation, negative_from, new_text)
+
+ pos = pos if pos is not None else pipe["positive"]
+ neg = neg if neg is not None else pipe["negative"]
+
+ new_pipe = {
+ "model": pipe["model"],
+ "positive": pos,
+ "negative": neg,
+ "vae": pipe["vae"],
+ "clip": samp_clip,
+
+ "samples": pipe["samples"],
+ "images": pipe["images"],
+ "seed": pipe["seed"],
+
+ "loader_settings": pipe["loader_settings"],
+ }
+ del pipe
+
+ return (new_pipe, new_pipe["positive"], new_pipe["negative"], samp_clip, { "ui": { "string": new_text } } )
+
+class ttN_pipeLoraStack:
+ version = '1.1.1'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ inputs = {
+ "required": {
+ "toggle": ([True, False],),
+ "mode": (["simple", "advanced"],),
+ "num_loras": ("INT", {"default": 1, "min": 0, "max": 20}),
+ },
+ "optional": {
+ "optional_pipe": ("PIPE_LINE", {"default": None}),
+ "model_override": ("MODEL",),
+ "clip_override": ("CLIP",),
+ "optional_lora_stack": ("LORA_STACK",),
+ },
+ "hidden": {
+ "ttNnodeVersion": (ttN_pipeLoraStack.version),
+ },
+ }
+
+ for i in range(1, 21):
+ inputs["optional"][f"lora_{i}_name"] = (["None"] + folder_paths.get_filename_list("loras"),{"default": "None"})
+ inputs["optional"][f"lora_{i}_strength"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
+ inputs["optional"][f"lora_{i}_model_strength"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
+ inputs["optional"][f"lora_{i}_clip_strength"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
+
+ return inputs
+
+
+ RETURN_TYPES = ("PIPE_LINE", "LORA_STACK",)
+ RETURN_NAMES = ("optional_pipe","lora_stack",)
+ FUNCTION = "stack"
+
+ CATEGORY = "ttN/pipe"
+
+ def stack(self, toggle, mode, num_loras, optional_pipe=None, lora_stack=None, model_override=None, clip_override=None, **kwargs):
+ if (toggle in [False, None, "False"]) or not kwargs:
+ return optional_pipe, None
+
+ loras = []
+
+ # Import Stack values
+ if lora_stack is not None:
+ loras.extend([l for l in lora_stack if l[0] != "None"])
+
+ # Import Lora values
+ for i in range(1, num_loras + 1):
+ lora_name = kwargs.get(f"lora_{i}_name")
+
+ if not lora_name or lora_name == "None":
+ continue
+
+ if mode == "simple":
+ lora_strength = float(kwargs.get(f"lora_{i}_strength"))
+ loras.append((lora_name, lora_strength, lora_strength))
+ elif mode == "advanced":
+ model_strength = float(kwargs.get(f"lora_{i}_model_strength"))
+ clip_strength = float(kwargs.get(f"lora_{i}_clip_strength"))
+ loras.append((lora_name, model_strength, clip_strength))
+
+ if not loras:
+ return optional_pipe, None
+
+ if loras and not optional_pipe:
+ return optional_pipe, loras
+
+ # Load Loras
+ model = model_override or optional_pipe.get("model")
+ clip = clip_override or optional_pipe.get("clip")
+
+ if not model or not clip:
+ return optional_pipe, loras
+
+ for lora in loras:
+ model, clip = ttNcache.load_lora(lora[0], model, clip, lora[1], lora[2])
+
+ new_pipe = {
+ "model": model,
+ "positive": optional_pipe["positive"],
+ "negative": optional_pipe["negative"],
+ "vae": optional_pipe["vae"],
+ "clip": clip,
+
+ "samples": optional_pipe["samples"],
+ "images": optional_pipe["images"],
+ "seed": optional_pipe["seed"],
+
+ "loader_settings": optional_pipe["loader_settings"],
+ }
+
+ del optional_pipe
+
+ return new_pipe, loras
+#---------------------------------------------------------------ttN/pipe END------------------------------------------------------------------------#
+
+#----------------------------------------------------------------misc START------------------------------------------------------------------------#
+WEIGHTED_SUM = "Weighted sum = ( A*(1-M) + B*M )"
+ADD_DIFFERENCE = "Add difference = ( A + (B-C)*M )"
+A_ONLY = "A Only"
+MODEL_INTERPOLATIONS = [WEIGHTED_SUM, ADD_DIFFERENCE, A_ONLY]
+FOLLOW = "Follow model interp"
+B_ONLY = "B Only"
+C_ONLY = "C Only"
+CLIP_INTERPOLATIONS = [FOLLOW, WEIGHTED_SUM, ADD_DIFFERENCE, A_ONLY, B_ONLY, C_ONLY]
+ABC = "ABC"
+
+class ttN_multiModelMerge:
+ version = '1.0.1'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "ckpt_A_name": (folder_paths.get_filename_list("checkpoints"), ),
+ "config_A_name": (["Default",] + folder_paths.get_filename_list("configs"), {"default": "Default"} ),
+ "ckpt_B_name": (["None",] + folder_paths.get_filename_list("checkpoints"), ),
+ "config_B_name": (["Default",] + folder_paths.get_filename_list("configs"), {"default": "Default"} ),
+ "ckpt_C_name": (["None",] + folder_paths.get_filename_list("checkpoints"), ),
+ "config_C_name": (["Default",] + folder_paths.get_filename_list("configs"), {"default": "Default"} ),
+
+ "model_interpolation": (MODEL_INTERPOLATIONS,),
+ "model_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
+
+ "clip_interpolation": (CLIP_INTERPOLATIONS,),
+ "clip_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
+
+ "save_model": (["True", "False"],{"default": "False"}),
+ "save_prefix": ("STRING", {"default": "checkpoints/ComfyUI"}),
+ },
+ "optional": {
+ "model_A_override": ("MODEL",),
+ "model_B_override": ("MODEL",),
+ "model_C_override": ("MODEL",),
+ "clip_A_override": ("CLIP",),
+ "clip_B_override": ("CLIP",),
+ "clip_C_override": ("CLIP",),
+ "optional_vae": ("VAE",),
+ },
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "ttNnodeVersion": ttN_multiModelMerge.version, "my_unique_id": "UNIQUE_ID"},
+ }
+
+ RETURN_TYPES = ("MODEL", "CLIP", "VAE",)
+ RETURN_NAMES = ("model", "clip", "vae",)
+ FUNCTION = "mergificate"
+
+ CATEGORY = "ttN"
+
+ def mergificate(self, ckpt_A_name, config_A_name, ckpt_B_name, config_B_name, ckpt_C_name, config_C_name,
+ model_interpolation, model_multiplier, clip_interpolation, clip_multiplier, save_model, save_prefix,
+ model_A_override=None, model_B_override=None, model_C_override=None,
+ clip_A_override=None, clip_B_override=None, clip_C_override=None,
+ optional_vae=None, prompt=None, extra_pnginfo=None, my_unique_id=None):
+
+ def required_assets(model_interpolation, clip_interpolation):
+ required = set(["model_A"])
+
+ if clip_interpolation in [A_ONLY, B_ONLY, C_ONLY]:
+ required.add(f"clip_{clip_interpolation[0]}")
+ elif clip_interpolation in [WEIGHTED_SUM, ADD_DIFFERENCE]:
+ required.update([f"clip_{letter}" for letter in ABC if letter in clip_interpolation])
+ elif clip_interpolation == FOLLOW:
+ required.add("clip_A")
+
+ if model_interpolation in [WEIGHTED_SUM, ADD_DIFFERENCE]:
+ letters = [letter for letter in ABC if letter in model_interpolation]
+ required.update([f"model_{letter}" for letter in letters])
+ if clip_interpolation == FOLLOW:
+ required.update([f"clip_{letter}" for letter in letters])
+
+ return sorted(list(required))
+
+ def _collect_letter(letter, required_list, model_override, clip_override, ckpt_name, config_name = None):
+ model, clip, loaded_clip = None, None, None
+ config_name = config_name
+
+ if f'model_{letter}' in required_list:
+ if model_override not in [None, "None"]:
+ model = model_override
+ else:
+ if ckpt_name not in [None, "None"]:
+ model, loaded_clip, _ = ttNcache.load_checkpoint(ckpt_name, config_name)
+ else:
+ e = f"Checkpoint name or model override not provided for model_{letter}.\nUnable to merge models using the following interpolation: {model_interpolation}"
+ ttNl(e).t(f'multiModelMerge [{my_unique_id}]').error().p().interrupt(e)
+
+
+
+ if f'clip_{letter}' in required_list:
+ if clip_override is not None:
+ clip = clip_override
+ elif loaded_clip is not None:
+ clip = loaded_clip
+ elif ckpt_name not in [None, "None"]:
+ _, clip, _ = ttNcache.load_checkpoint(ckpt_name, config_name)
+ else:
+ e = f"Checkpoint name or clip override not provided for clip_{letter}.\nUnable to merge clips using the following interpolation: {clip_interpolation}"
+ ttNl(e).t(f'multiModelMerge [{my_unique_id}]').error().p().interrupt(e)
+
+ return model, clip
+
+
+ def merge(base_model, base_strength, patch_model, patch_strength):
+ m = base_model.clone()
+ kp = patch_model.get_key_patches("diffusion_model.")
+ for k in kp:
+ m.add_patches({k: kp[k]}, patch_strength, base_strength)
+ return m
+
+ def clip_merge(base_clip, base_strength, patch_clip, patch_strength):
+ m = base_clip.clone()
+ kp = patch_clip.get_key_patches()
+ for k in kp:
+ if k.endswith(".position_ids") or k.endswith(".logit_scale"):
+ continue
+ m.add_patches({k: kp[k]}, patch_strength, base_strength)
+ return m
+
+ def _add_assets(a1, a2, is_clip=False, multiplier=1.0, weighted=False):
+ if is_clip:
+ if weighted:
+ return clip_merge(a1, (1.0 - multiplier), a2, multiplier)
+ else:
+ return clip_merge(a1, 1.0, a2, multiplier)
+ else:
+ if weighted:
+ return merge(a1, (1.0 - multiplier), a2, multiplier)
+ else:
+ return merge(a1, 1.0, a2, multiplier)
+
+ def _subtract_assets(a1, a2, is_clip=False, multiplier=1.0):
+ if is_clip:
+ return clip_merge(a1, 1.0, a2, -multiplier)
+ else:
+ return merge(a1, 1.0, a2, -multiplier)
+
+ required_list = required_assets(model_interpolation, clip_interpolation)
+ model_A, clip_A = _collect_letter("A", required_list, model_A_override, clip_A_override, ckpt_A_name, config_A_name)
+ model_B, clip_B = _collect_letter("B", required_list, model_B_override, clip_B_override, ckpt_B_name, config_B_name)
+ model_C, clip_C = _collect_letter("C", required_list, model_C_override, clip_C_override, ckpt_C_name, config_C_name)
+
+ if (model_interpolation == A_ONLY):
+ model = model_A
+ if (model_interpolation == WEIGHTED_SUM):
+ model = _add_assets(model_A, model_B, False, model_multiplier, True)
+ if (model_interpolation == ADD_DIFFERENCE):
+ model = _add_assets(model_A, _subtract_assets(model_B, model_C), False, model_multiplier)
+
+ if (clip_interpolation == FOLLOW):
+ clip_interpolation = model_interpolation
+ if (clip_interpolation == A_ONLY):
+ clip = clip_A
+ if (clip_interpolation == B_ONLY):
+ clip = clip_B
+ if (clip_interpolation == C_ONLY):
+ clip = clip_C
+ if (clip_interpolation == WEIGHTED_SUM):
+ clip = _add_assets(clip_A, clip_B, True, clip_multiplier, True)
+ if (clip_interpolation == ADD_DIFFERENCE):
+ clip = _add_assets(clip_A, _subtract_assets(clip_B, clip_C, True), True, clip_multiplier)
+
+
+ if optional_vae not in ["None", None]:
+ vae_sd = optional_vae.get_sd()
+ vae = optional_vae
+ else:
+ vae_sd = {}
+ vae = None
+
+ if save_model == "True":
+ full_output_folder, filename, counter, subfolder, save_prefix = folder_paths.get_save_image_path(save_prefix, folder_paths.get_output_directory())
+
+ prompt_info = ""
+ if prompt is not None:
+ prompt_info = json.dumps(prompt)
+
+ metadata = {}
+
+ enable_modelspec = True
+ if isinstance(model.model, comfy.model_base.SDXL):
+ metadata["modelspec.architecture"] = "stable-diffusion-xl-v1-base"
+ elif isinstance(model.model, comfy.model_base.SDXLRefiner):
+ metadata["modelspec.architecture"] = "stable-diffusion-xl-v1-refiner"
+ else:
+ enable_modelspec = False
+
+ if enable_modelspec:
+ metadata["modelspec.sai_model_spec"] = "1.0.0"
+ metadata["modelspec.implementation"] = "sgm"
+ metadata["modelspec.title"] = "{} {}".format(filename, counter)
+
+ if model.model.model_type == comfy.model_base.ModelType.EPS:
+ metadata["modelspec.predict_key"] = "epsilon"
+ elif model.model.model_type == comfy.model_base.ModelType.V_PREDICTION:
+ metadata["modelspec.predict_key"] = "v"
+
+ if not args.disable_metadata:
+ metadata["prompt"] = prompt_info
+ if extra_pnginfo is not None:
+ for x in extra_pnginfo:
+ metadata[x] = json.dumps(extra_pnginfo[x])
+
+ output_checkpoint = f"{filename}_{counter:05}_.safetensors"
+ output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
+
+ comfy.model_management.load_models_gpu([model, clip.load_model()])
+ sd = model.model.state_dict_for_saving(clip.get_sd(), vae_sd)
+ comfy.utils.save_torch_file(sd, output_checkpoint, metadata=metadata)
+
+ return (model, clip, vae)
+#-----------------------------------------------------------------misc END-------------------------------------------------------------------------#
+
+#---------------------------------------------------------------ttN/text START----------------------------------------------------------------------#
+class ttN_text:
+ version = '1.0.0'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "text": ("STRING", {"default": "", "multiline": True}),
+ },
+ "hidden": {"ttNnodeVersion": ttN_text.version},
+ }
+
+ RETURN_TYPES = ("STRING",)
+ RETURN_NAMES = ("text",)
+ FUNCTION = "conmeow"
+
+ CATEGORY = "ttN/text"
+
+ @staticmethod
+ def conmeow(text):
+ return text,
+
+class ttN_textDebug:
+ version = '1.0.'
+ def __init__(self):
+ self.num = 0
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "print_to_console": ([False, True],),
+ "console_title": ("STRING", {"default": ""}),
+ "execute": (["Always", "On Change"],),
+ "text": ("STRING", {"default": '', "multiline": True, "forceInput": True}),
+ },
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
+ "ttNnodeVersion": ttN_textDebug.version},
+ }
+
+ RETURN_TYPES = ("STRING",)
+ RETURN_NAMES = ("text",)
+ FUNCTION = "write"
+ OUTPUT_NODE = True
+
+ CATEGORY = "ttN/text"
+
+ def write(self, print_to_console, console_title, execute, text, prompt, extra_pnginfo, my_unique_id):
+ if execute == "Always":
+ def IS_CHANGED(self):
+ self.num += 1 if self.num == 0 else -1
+ return self.num
+ setattr(self.__class__, 'IS_CHANGED', IS_CHANGED)
+
+ if execute == "On Change":
+ if hasattr(self.__class__, 'IS_CHANGED'):
+ delattr(self.__class__, 'IS_CHANGED')
+
+ if print_to_console == True:
+ if console_title != "":
+ ttNl(text).t(f'textDebug[{my_unique_id}] - {CC.VIOLET}{console_title}').p()
+ else:
+ input_node = prompt[my_unique_id]["inputs"]["text"]
+
+ input_from = None
+ for node in extra_pnginfo["workflow"]["nodes"]:
+ if node['id'] == int(input_node[0]):
+ input_from = node['outputs'][input_node[1]].get('label')
+
+ if input_from == None:
+ input_from = node['outputs'][input_node[1]].get('name')
+
+ ttNl(text).t(f'textDebug[{my_unique_id}] - {CC.VIOLET}{input_from}').p()
+
+ return {"ui": {"text": text},
+ "result": (text,)}
+
+class ttN_concat:
+ version = '1.0.0'
+ def __init__(self):
+ pass
+ """
+ Concatenate 2 strings
+ """
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "text1": ("STRING", {"multiline": True, "default": ''}),
+ "text2": ("STRING", {"multiline": True, "default": ''}),
+ "text3": ("STRING", {"multiline": True, "default": ''}),
+ "delimiter": ("STRING", {"default":",","multiline": False}),
+ },
+ "hidden": {"ttNnodeVersion": ttN_concat.version},
+ }
+
+ RETURN_TYPES = ("STRING",)
+ RETURN_NAMES = ("concat",)
+ FUNCTION = "conmeow"
+
+ CATEGORY = "ttN/text"
+
+ def conmeow(self, text1='', text2='', text3='', delimiter=''):
+ text1 = '' if text1 == 'undefined' else text1
+ text2 = '' if text2 == 'undefined' else text2
+ text3 = '' if text3 == 'undefined' else text3
+
+ concat = delimiter.join([text1, text2, text3])
+
+ return (concat,)
+
+class ttN_text3BOX_3WAYconcat:
+ version = '1.0.0'
+ def __init__(self):
+ pass
+ """
+ Concatenate 3 strings, in various ways.
+ """
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "text1": ("STRING", {"multiline": True, "default": ''}),
+ "text2": ("STRING", {"multiline": True, "default": ''}),
+ "text3": ("STRING", {"multiline": True, "default": ''}),
+ "delimiter": ("STRING", {"default":",","multiline": False}),
+ },
+ "hidden": {"ttNnodeVersion": ttN_text3BOX_3WAYconcat.version},
+ }
+
+ RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING",)
+ RETURN_NAMES = ("text1", "text2", "text3", "1 & 2", "1 & 3", "2 & 3", "concat",)
+ FUNCTION = "conmeow"
+
+ CATEGORY = "ttN/text"
+
+ def conmeow(self, text1='', text2='', text3='', delimiter=''):
+ text1 = '' if text1 == 'undefined' else text1
+ text2 = '' if text2 == 'undefined' else text2
+ text3 = '' if text3 == 'undefined' else text3
+
+ t_1n2 = delimiter.join([text1, text2])
+ t_1n3 = delimiter.join([text1, text3])
+ t_2n3 = delimiter.join([text2, text3])
+ concat = delimiter.join([text1, text2, text3])
+
+ return text1, text2, text3, t_1n2, t_1n3, t_2n3, concat
+
+class ttN_text7BOX_concat:
+ version = '1.0.0'
+ def __init__(self):
+ pass
+ """
+ Concatenate many strings
+ """
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "text1": ("STRING", {"multiline": True, "default": ''}),
+ "text2": ("STRING", {"multiline": True, "default": ''}),
+ "text3": ("STRING", {"multiline": True, "default": ''}),
+ "text4": ("STRING", {"multiline": True, "default": ''}),
+ "text5": ("STRING", {"multiline": True, "default": ''}),
+ "text6": ("STRING", {"multiline": True, "default": ''}),
+ "text7": ("STRING", {"multiline": True, "default": ''}),
+ "delimiter": ("STRING", {"default":",","multiline": False}),
+ },
+ "hidden": {"ttNnodeVersion": ttN_text7BOX_concat.version},
+ }
+
+ RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING",)
+ RETURN_NAMES = ("text1", "text2", "text3", "text4", "text5", "text6", "text7", "concat",)
+ FUNCTION = "conmeow"
+
+ CATEGORY = "ttN/text"
+
+ def conmeow(self, text1, text2, text3, text4, text5, text6, text7, delimiter):
+ text1 = '' if text1 == 'undefined' else text1
+ text2 = '' if text2 == 'undefined' else text2
+ text3 = '' if text3 == 'undefined' else text3
+ text4 = '' if text4 == 'undefined' else text4
+ text5 = '' if text5 == 'undefined' else text5
+ text6 = '' if text6 == 'undefined' else text6
+ text7 = '' if text7 == 'undefined' else text7
+
+ texts = [text1, text2, text3, text4, text5, text6, text7]
+ concat = delimiter.join(text for text in texts if text)
+ return text1, text2, text3, text4, text5, text6, text7, concat
+#---------------------------------------------------------------ttN/text END------------------------------------------------------------------------#
+
+
+#---------------------------------------------------------------ttN/util START----------------------------------------------------------------------#
+class ttN_INT:
+ version = '1.0.0'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "int": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ },
+ "hidden": {"ttNnodeVersion": ttN_INT.version},
+ }
+
+ RETURN_TYPES = ("INT", "FLOAT", "STRING",)
+ RETURN_NAMES = ("int", "float", "text",)
+ FUNCTION = "convert"
+
+ CATEGORY = "ttN/util"
+
+ @staticmethod
+ def convert(int):
+ return int, float(int), str(int)
+
+class ttN_FLOAT:
+ version = '1.0.0'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "float": ("FLOAT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ },
+ "hidden": {"ttNnodeVersion": ttN_FLOAT.version},
+ }
+
+ RETURN_TYPES = ("FLOAT", "INT", "STRING",)
+ RETURN_NAMES = ("float", "int", "text",)
+ FUNCTION = "convert"
+
+ CATEGORY = "ttN/util"
+
+ @staticmethod
+ def convert(float):
+ return float, int(float), str(float)
+
+class ttN_SEED:
+ version = '1.0.0'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ },
+ "hidden": {"ttNnodeVersion": ttN_SEED.version},
+ }
+
+ RETURN_TYPES = ("INT",)
+ RETURN_NAMES = ("seed",)
+ FUNCTION = "plant"
+ OUTPUT_NODE = True
+
+ CATEGORY = "ttN/util"
+
+ @staticmethod
+ def plant(seed):
+ return seed,
+#---------------------------------------------------------------ttN/util End------------------------------------------------------------------------#
+
+
+#---------------------------------------------------------------ttN/image START---------------------------------------------------------------------#
+#class ttN_imageREMBG:
+try:
+ from rembg import remove
+ class ttN_imageREMBG:
+ version = '1.0.0'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "image": ("IMAGE",),
+ "image_output": (["Hide", "Preview", "Save", "Hide/Save"],{"default": "Preview"}),
+ "save_prefix": ("STRING", {"default": "ComfyUI"}),
+ },
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
+ "ttNnodeVersion": ttN_imageREMBG.version},
+ }
+
+
+ RETURN_TYPES = ("IMAGE", "MASK")
+ RETURN_NAMES = ("image", "mask")
+ FUNCTION = "remove_background"
+ CATEGORY = "ttN/image"
+ OUTPUT_NODE = True
+
+ def remove_background(self, image, image_output, save_prefix, prompt, extra_pnginfo, my_unique_id):
+ image = remove(ttNsampler.tensor2pil(image))
+ tensor = ttNsampler.pil2tensor(image)
+
+ #Get alpha mask
+ if image.getbands() != ("R", "G", "B", "A"):
+ image = image.convert("RGBA")
+ mask = None
+ if "A" in image.getbands():
+ mask = np.array(image.getchannel("A")).astype(np.float32) / 255.0
+ mask = torch.from_numpy(mask)
+ mask = 1. - mask
+ else:
+ mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
+
+ if image_output == "Disabled":
+ results = []
+ else:
+ ttN_save = ttNsave(my_unique_id, prompt, extra_pnginfo)
+ results = ttN_save.images(tensor, save_prefix, image_output)
+
+ if image_output in ("Hide", "Hide/Save"):
+ return (tensor, mask)
+
+ # Output image results to ui and node outputs
+ return {"ui": {"images": results},
+ "result": (tensor, mask)}
+except:
+ class ttN_imageREMBG:
+ version = '0.0.0'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "error": ("STRING",{"default": "RemBG is not installed", "multiline": False, 'readonly': True}),
+ "link": ("STRING",{"default": "https://github.com/danielgatis/rembg", "multiline": False}),
+ },
+ "hidden": {"ttNnodeVersion": ttN_imageREMBG.version},
+ }
+
+
+ RETURN_TYPES = ("")
+ FUNCTION = "remove_background"
+ CATEGORY = "ttN/image"
+
+ def remove_background(error):
+ return None
+
+class ttN_imageOUPUT:
+ version = '1.1.0'
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "image": ("IMAGE",),
+ "image_output": (["Hide", "Preview", "Save", "Hide/Save"],{"default": "Preview"}),
+ "output_path": ("STRING", {"default": folder_paths.get_output_directory(), "multiline": False}),
+ "save_prefix": ("STRING", {"default": "ComfyUI"}),
+ "number_padding": (["None", 2, 3, 4, 5, 6, 7, 8, 9],{"default": 5}),
+ "file_type": (["PNG", "JPG", "JPEG", "BMP", "TIFF", "TIF"],{"default": "PNG"}),
+ "overwrite_existing": (["True", "False"],{"default": "False"}),
+ "embed_workflow": (["True", "False"],),
+
+ },
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
+ "ttNnodeVersion": ttN_imageOUPUT.version},
+ }
+
+ RETURN_TYPES = ("IMAGE",)
+ RETURN_NAMES = ("image",)
+ FUNCTION = "output"
+ CATEGORY = "ttN/image"
+ OUTPUT_NODE = True
+
+ def output(self, image, image_output, output_path, save_prefix, number_padding, file_type, overwrite_existing, embed_workflow, prompt, extra_pnginfo, my_unique_id):
+ ttN_save = ttNsave(my_unique_id, prompt, extra_pnginfo, number_padding, overwrite_existing, output_path)
+ results = ttN_save.images(image, save_prefix, image_output, embed_workflow, file_type.lower())
+
+ if image_output in ("Hide", "Hide/Save"):
+ return (image,)
+
+ # Output image results to ui and node outputs
+ return {"ui": {"images": results},
+ "result": (image,)}
+
+class ttN_modelScale:
+ version = '1.0.3'
+ upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos", "bislerp"]
+ crop_methods = ["disabled", "center"]
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "model_name": (folder_paths.get_filename_list("upscale_models"),),
+ "image": ("IMAGE",),
+ "rescale_after_model": ([False, True],{"default": True}),
+ "rescale_method": (s.upscale_methods,),
+ "rescale": (["by percentage", "to Width/Height", 'to longer side - maintain aspect'],),
+ "percent": ("INT", {"default": 50, "min": 0, "max": 1000, "step": 1}),
+ "width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "longer_side": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "crop": (s.crop_methods,),
+ "image_output": (["Hide", "Preview", "Save", "Hide/Save"],),
+ "save_prefix": ("STRING", {"default": "ComfyUI"}),
+ "output_latent": ([False, True],{"default": True}),
+ "vae": ("VAE",),},
+ "hidden": { "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
+ "ttNnodeVersion": ttN_modelScale.version},
+ }
+
+ RETURN_TYPES = ("LATENT", "IMAGE",)
+ RETURN_NAMES = ("latent", 'image',)
+
+ FUNCTION = "upscale"
+ CATEGORY = "ttN/image"
+ OUTPUT_NODE = True
+
+ def vae_encode_crop_pixels(self, pixels):
+ x = (pixels.shape[1] // 8) * 8
+ y = (pixels.shape[2] // 8) * 8
+ if pixels.shape[1] != x or pixels.shape[2] != y:
+ x_offset = (pixels.shape[1] % 8) // 2
+ y_offset = (pixels.shape[2] % 8) // 2
+ pixels = pixels[:, x_offset:x + x_offset, y_offset:y + y_offset, :]
+ return pixels
+
+ def upscale(self, model_name, image, rescale_after_model, rescale_method, rescale, percent, width, height, longer_side, crop, image_output, save_prefix, output_latent, vae, prompt=None, extra_pnginfo=None, my_unique_id=None):
+ # Load Model
+ model_path = folder_paths.get_full_path("upscale_models", model_name)
+ sd = comfy.utils.load_torch_file(model_path, safe_load=True)
+ upscale_model = model_loading.load_state_dict(sd).eval()
+
+ # Model upscale
+ device = comfy.model_management.get_torch_device()
+ upscale_model.to(device)
+ in_img = image.movedim(-1,-3).to(device)
+
+ tile = 128 + 64
+ overlap = 8
+ steps = in_img.shape[0] * comfy.utils.get_tiled_scale_steps(in_img.shape[3], in_img.shape[2], tile_x=tile, tile_y=tile, overlap=overlap)
+ pbar = comfy.utils.ProgressBar(steps)
+ s = comfy.utils.tiled_scale(in_img, lambda a: upscale_model(a), tile_x=tile, tile_y=tile, overlap=overlap, upscale_amount=upscale_model.scale, pbar=pbar)
+ upscale_model.cpu()
+ s = torch.clamp(s.movedim(-3,-1), min=0, max=1.0)
+
+ # Post Model Rescale
+ if rescale_after_model == True:
+ samples = s.movedim(-1, 1)
+ orig_height = samples.shape[2]
+ orig_width = samples.shape[3]
+ if rescale == "by percentage" and percent != 0:
+ height = percent / 100 * orig_height
+ width = percent / 100 * orig_width
+ if (width > MAX_RESOLUTION):
+ width = MAX_RESOLUTION
+ if (height > MAX_RESOLUTION):
+ height = MAX_RESOLUTION
+
+ width = ttNsampler.enforce_mul_of_64(width)
+ height = ttNsampler.enforce_mul_of_64(height)
+ elif rescale == "to longer side - maintain aspect":
+ longer_side = ttNsampler.enforce_mul_of_64(longer_side)
+ if orig_width > orig_height:
+ width, height = longer_side, ttNsampler.enforce_mul_of_64(longer_side * orig_height / orig_width)
+ else:
+ width, height = ttNsampler.enforce_mul_of_64(longer_side * orig_width / orig_height), longer_side
+
+
+ s = comfy.utils.common_upscale(samples, width, height, rescale_method, crop)
+ s = s.movedim(1,-1)
+
+ # vae encode
+ if output_latent == True:
+ pixels = self.vae_encode_crop_pixels(s)
+ t = vae.encode(pixels[:,:,:,:3])
+ else:
+ t = None
+
+ ttN_save = ttNsave(my_unique_id, prompt, extra_pnginfo)
+ results = ttN_save.images(s, save_prefix, image_output)
+
+ if image_output in ("Hide", "Hide/Save"):
+ return ({"samples":t}, s,)
+
+ return {"ui": {"images": results},
+ "result": ({"samples":t}, s,)}
+#---------------------------------------------------------------ttN/image END-----------------------------------------------------------------------#
+
+TTN_VERSIONS = {
+ "tinyterraNodes": ttN_version,
+ "pipeLoader": ttN_TSC_pipeLoader.version,
+ "pipeKSampler": ttN_TSC_pipeKSampler.version,
+ "pipeKSamplerAdvanced": ttN_pipeKSamplerAdvanced.version,
+ "pipeLoaderSDXL": ttN_pipeLoaderSDXL.version,
+ "pipeKSamplerSDXL": ttN_pipeKSamplerSDXL.version,
+ "pipeIN": ttN_pipe_IN.version,
+ "pipeOUT": ttN_pipe_OUT.version,
+ "pipeEDIT": ttN_pipe_EDIT.version,
+ "pipe2BASIC": ttN_pipe_2BASIC.version,
+ "pipe2DETAILER": ttN_pipe_2DETAILER.version,
+ "xyPlot": ttN_XYPlot.version,
+ "pipeEncodeConcat": ttN_pipeEncodeConcat.version,
+ "multiLoraStack": ttN_pipeLoraStack.version,
+ "multiModelMerge": ttN_multiModelMerge.version,
+ "text": ttN_text.version,
+ "textDebug": ttN_textDebug.version,
+ "concat": ttN_concat.version,
+ "text3BOX_3WAYconcat": ttN_text3BOX_3WAYconcat.version,
+ "text7BOX_concat": ttN_text7BOX_concat.version,
+ "imageOutput": ttN_imageOUPUT.version,
+ "imageREMBG": ttN_imageREMBG.version,
+ "hiresfixScale": ttN_modelScale.version,
+ "int": ttN_INT.version,
+ "float": ttN_FLOAT.version,
+ "seed": ttN_SEED.version
+}
+NODE_CLASS_MAPPINGS = {
+ #ttN/pipe
+ "ttN pipeLoader": ttN_TSC_pipeLoader,
+ "ttN pipeKSampler": ttN_TSC_pipeKSampler,
+ "ttN pipeKSamplerAdvanced": ttN_pipeKSamplerAdvanced,
+ "ttN pipeLoaderSDXL": ttN_pipeLoaderSDXL,
+ "ttN pipeKSamplerSDXL": ttN_pipeKSamplerSDXL,
+ "ttN xyPlot": ttN_XYPlot,
+ "ttN pipeIN": ttN_pipe_IN,
+ "ttN pipeOUT": ttN_pipe_OUT,
+ "ttN pipeEDIT": ttN_pipe_EDIT,
+ "ttN pipe2BASIC": ttN_pipe_2BASIC,
+ "ttN pipe2DETAILER": ttN_pipe_2DETAILER,
+ "ttN pipeEncodeConcat": ttN_pipeEncodeConcat,
+ "ttN pipeLoraStack": ttN_pipeLoraStack,
+
+ #ttN/encode
+ "ttN multiModelMerge": ttN_multiModelMerge,
+
+ #ttN/text
+ "ttN text": ttN_text,
+ "ttN textDebug": ttN_textDebug,
+ "ttN concat": ttN_concat,
+ "ttN text3BOX_3WAYconcat": ttN_text3BOX_3WAYconcat,
+ "ttN text7BOX_concat": ttN_text7BOX_concat,
+
+ #ttN/image
+ "ttN imageOutput": ttN_imageOUPUT,
+ "ttN imageREMBG": ttN_imageREMBG,
+ "ttN hiresfixScale": ttN_modelScale,
+
+ #ttN/util
+ "ttN int": ttN_INT,
+ "ttN float": ttN_FLOAT,
+ "ttN seed": ttN_SEED
+}
+NODE_DISPLAY_NAME_MAPPINGS = {
+ #ttN/pipe
+ "ttN pipeLoader": "pipeLoader",
+ "ttN pipeKSampler": "pipeKSampler",
+ "ttN pipeKSamplerAdvanced": "pipeKSamplerAdvanced",
+ "ttN pipeLoaderSDXL": "pipeLoaderSDXL",
+ "ttN pipeKSamplerSDXL": "pipeKSamplerSDXL",
+ "ttN xyPlot": "xyPlot",
+ "ttN pipeIN": "pipeIN (Legacy)",
+ "ttN pipeOUT": "pipeOUT (Legacy)",
+ "ttN pipeEDIT": "pipeEDIT",
+ "ttN pipe2BASIC": "pipe > basic_pipe",
+ "ttN pipe2DETAILER": "pipe > detailer_pipe",
+ "ttN pipeEncodeConcat": "pipeEncodeConcat",
+ "ttN pipeLoraStack": "pipeLoraStack",
+
+ #ttN/misc
+ "ttN multiModelMerge": "multiModelMerge",
+
+ #ttN/text
+ "ttN text": "text",
+ "ttN textDebug": "textDebug",
+ "ttN concat": "textConcat",
+ "ttN text7BOX_concat": "7x TXT Loader Concat",
+ "ttN text3BOX_3WAYconcat": "3x TXT Loader MultiConcat",
+
+ #ttN/image
+ "ttN imageREMBG": "imageRemBG",
+ "ttN imageOutput": "imageOutput",
+ "ttN hiresfixScale": "hiresfixScale",
+
+ #ttN/util
+ "ttN int": "int",
+ "ttN float": "float",
+ "ttN seed": "seed"
+}
+
+ttNl('Loaded').full().p()
+
+#---------------------------------------------------------------------------------------------------------------------------------------------------#
+# (KSampler Modified from TSC Efficiency Nodes) - https://github.com/LucianoCirino/efficiency-nodes-comfyui #
+# (upscale from QualityOfLifeSuite_Omar92) - https://github.com/omar92/ComfyUI-QualityOfLifeSuit_Omar92 #
+# (Node weights from BlenderNeko/ComfyUI_ADV_CLIP_emb) - https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb #
+# (misc. from WAS node Suite) - https://github.com/WASasquatch/was-node-suite-comfyui #
+#---------------------------------------------------------------------------------------------------------------------------------------------------#
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/ttNdev.py b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/ttNdev.py
new file mode 100644
index 0000000000000000000000000000000000000000..462001a052139e9a6e03d31df0963f8d13290cc6
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/ttNdev.py
@@ -0,0 +1,128 @@
+import folder_paths
+import comfy.samplers
+
+MAX_RESOLUTION=8192
+
+# in_dev - likely broken
+class ttN_debugInput:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"console_title": ("STRING", {"default": "ttN INPUT DEBUG"}),},
+ "optional": {"debug": ("", {"default": None}),}
+ }
+
+ RETURN_TYPES = tuple()
+ RETURN_NAMES = tuple()
+ FUNCTION = "debug"
+ CATEGORY = "ttN/dev"
+ OUTPUT_NODE = True
+
+ def debug(_, **kwargs):
+ for key, value in kwargs.items():
+ if key == "console_title":
+ print(value)
+ else:
+ print(f"{key}: {value}")
+ return tuple()
+
+class ttN_compareInput:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"console_title": ("STRING", {"default": "ttN INPUT COMPARE"}),},
+ "optional": {"debug": ("", {"default": None}),
+ "debug2": ("", {"default": None}),}
+ }
+
+ RETURN_TYPES = tuple()
+ RETURN_NAMES = tuple()
+ FUNCTION = "debug"
+ CATEGORY = "ttN/dev"
+ OUTPUT_NODE = True
+
+ def debug(_, **kwargs):
+
+ values = []
+ for key, value in kwargs.items():
+ if key == "console_title":
+ print(value)
+ else:
+ print(f"{key}: {value}")
+ values.append(value)
+
+ return tuple()
+
+class ttN_busIN:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "lane_0": ("",),
+ }}
+
+ RETURN_TYPES = ("BUS_LINE",)
+ RETURN_NAMES = ("bus_line",)
+ FUNCTION = "roundnround"
+
+ CATEGORY = "ttN/dev"
+
+ @staticmethod
+ def roundnround(*args, **kwargs):
+ bus_line = []
+ for key, value in kwargs.items():
+ bus_line.append(value)
+ print("busIN + kw:--",tuple(bus_line))
+
+ return (tuple(bus_line),)
+
+class ttN_busOUT:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "bus_line": ("BUS_LINE",),
+ }}
+
+ RETURN_TYPES = ()
+ FUNCTION = "roundnround"
+
+ CATEGORY = "ttN/dev"
+
+ @staticmethod
+ def roundnround(bus_line):
+ print("busOUT:--",bus_line)
+ return (bus_line,)
+
+class ttN_seedDebug:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "ttNseed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ }}
+
+ RETURN_TYPES = ("INT",)
+ RETURN_NAMES = ("seed",)
+ FUNCTION = "plant"
+
+ CATEGORY = "ttN/dev"
+
+ @staticmethod
+ def plant(ttNseed, *args, **kwargs):
+ print('Seed:', ttNseed)
+ print('args:', args)
+ print('kwargs:',kwargs)
+ return (ttNseed,)
+
+NODE_CLASS_MAPPINGS = {
+ "ttN debugInput": ttN_debugInput,
+ "ttN compareInput": ttN_compareInput,
+ "ttN busIN": ttN_busIN,
+ "ttN busOUT": ttN_busOUT,
+ "ttN seedDebug": ttN_seedDebug,
+
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "ttN debugInput": "debugInput",
+ "ttN compareInput": "compareInput",
+ "ttN busIN": "busIN",
+ "ttN busOUT": "busOUT",
+ "ttN seedDebug": "seedDebug",
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_BasicSDXL.json b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_BasicSDXL.json
new file mode 100644
index 0000000000000000000000000000000000000000..0dc59b81c8564e78a730e91a69a8494a480d129e
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_BasicSDXL.json
@@ -0,0 +1,699 @@
+{
+ "last_node_id": 195,
+ "last_link_id": 489,
+ "nodes": [
+ {
+ "id": 188,
+ "type": "ttN text",
+ "pos": [
+ -450,
+ 590
+ ],
+ "size": {
+ "0": 400,
+ "1": 200
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "text",
+ "type": "STRING",
+ "links": [
+ 484,
+ 485
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN text",
+ "ttNnodeVersion": "1.0.0"
+ },
+ "widgets_values": [
+ "text, watermark"
+ ],
+ "color": "#322",
+ "bgcolor": "#533"
+ },
+ {
+ "id": 195,
+ "type": "ttN pipeKSamplerAdvanced",
+ "pos": [
+ 900,
+ -70
+ ],
+ "size": [
+ 453.6000061035156,
+ 850
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 488
+ },
+ {
+ "name": "optional_model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "optional_positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_latent",
+ "type": "LATENT",
+ "link": 489
+ },
+ {
+ "name": "optional_vae",
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ },
+ {
+ "name": "xyPlot",
+ "type": "XYPLOT",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "title": "pipeKSamplerAdvanced - Refiner",
+ "properties": {
+ "Node name for S&R": "ttN pipeKSamplerAdvanced",
+ "ttNnodeVersion": "1.0.5"
+ },
+ "widgets_values": [
+ "None",
+ 1,
+ 1,
+ "None",
+ 2,
+ "disabled",
+ "Sample",
+ "disable",
+ 20,
+ 9.5,
+ "uni_pc_bh2",
+ "normal",
+ 14,
+ 60,
+ "disable",
+ "Save",
+ "ComfyUI",
+ 425870239082281,
+ "randomize"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 192,
+ "type": "ttN pipeLoader",
+ "pos": [
+ 10,
+ -70
+ ],
+ "size": [
+ 380,
+ 626
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ },
+ {
+ "name": "positive",
+ "type": "STRING",
+ "link": 483,
+ "widget": {
+ "name": "positive",
+ "config": [
+ "STRING",
+ {
+ "default": "Positive",
+ "multiline": true
+ }
+ ]
+ }
+ },
+ {
+ "name": "negative",
+ "type": "STRING",
+ "link": 484,
+ "widget": {
+ "name": "negative",
+ "config": [
+ "STRING",
+ {
+ "default": "Negative",
+ "multiline": true
+ }
+ ]
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": [
+ 488
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "title": "pipeLoader - Refiner",
+ "properties": {
+ "Node name for S&R": "ttN pipeLoader",
+ "ttNnodeVersion": "1.0.3"
+ },
+ "widgets_values": [
+ "sd_xl_refiner_1.0.safetensors",
+ "Baked VAE",
+ -2,
+ "None",
+ 0.5,
+ 1,
+ "None",
+ 1,
+ 1,
+ "None",
+ 1,
+ 1,
+ "emb",
+ "none",
+ "comfy",
+ "Negative",
+ "none",
+ "comfy",
+ 1024,
+ 1024,
+ 1,
+ 7,
+ "fixed"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 193,
+ "type": "ttN pipeLoader",
+ "pos": [
+ 10,
+ 620
+ ],
+ "size": [
+ 380,
+ 674
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ },
+ {
+ "name": "positive",
+ "type": "STRING",
+ "link": 486,
+ "widget": {
+ "name": "positive",
+ "config": [
+ "STRING",
+ {
+ "default": "Positive",
+ "multiline": true
+ }
+ ]
+ }
+ },
+ {
+ "name": "negative",
+ "type": "STRING",
+ "link": 485,
+ "widget": {
+ "name": "negative",
+ "config": [
+ "STRING",
+ {
+ "default": "Negative",
+ "multiline": true
+ }
+ ]
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": [
+ 487
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "title": "pipeLoader - Base",
+ "properties": {
+ "Node name for S&R": "ttN pipeLoader",
+ "ttNnodeVersion": "1.0.3"
+ },
+ "widgets_values": [
+ "sd_xl_base_1.0.safetensors",
+ "sdxl_vae.safetensors",
+ -2,
+ "sd_xl_offset_example-lora_1.0.safetensors",
+ 0.5,
+ 1,
+ "None",
+ 1,
+ 1,
+ "None",
+ 1,
+ 1,
+ "big tree",
+ "none",
+ "comfy",
+ "Negative",
+ "none",
+ "comfy",
+ 1024,
+ 1024,
+ 1,
+ 7,
+ "fixed"
+ ],
+ "color": "#232",
+ "bgcolor": "#353"
+ },
+ {
+ "id": 194,
+ "type": "ttN pipeKSamplerAdvanced",
+ "pos": [
+ 410,
+ 620
+ ],
+ "size": [
+ 424.5285445098874,
+ 530
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 487
+ },
+ {
+ "name": "optional_model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "optional_positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_latent",
+ "type": "LATENT",
+ "link": null
+ },
+ {
+ "name": "optional_vae",
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ },
+ {
+ "name": "xyPlot",
+ "type": "XYPLOT",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 489
+ ],
+ "shape": 3,
+ "slot_index": 4
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "title": "pipeKSamplerAdvanced - Base",
+ "properties": {
+ "Node name for S&R": "ttN pipeKSamplerAdvanced",
+ "ttNnodeVersion": "1.0.5"
+ },
+ "widgets_values": [
+ "None",
+ 1,
+ 1,
+ "None",
+ 2,
+ "disabled",
+ "Sample",
+ "enable",
+ 20,
+ 9.5,
+ "uni_pc_bh2",
+ "normal",
+ 0,
+ 14,
+ "enable",
+ "Hide",
+ "ComfyUI",
+ 319330427537704,
+ "randomize"
+ ],
+ "color": "#232",
+ "bgcolor": "#353"
+ },
+ {
+ "id": 187,
+ "type": "ttN text",
+ "pos": [
+ -450,
+ 340
+ ],
+ "size": {
+ "0": 400,
+ "1": 200
+ },
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "text",
+ "type": "STRING",
+ "links": [
+ 483,
+ 486
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN text",
+ "ttNnodeVersion": "1.0.0"
+ },
+ "widgets_values": [
+ "evening sunset scenery blue sky nature, glass bottle with a galaxy in it"
+ ],
+ "color": "#232",
+ "bgcolor": "#353"
+ }
+ ],
+ "links": [
+ [
+ 483,
+ 187,
+ 0,
+ 192,
+ 1,
+ "STRING"
+ ],
+ [
+ 484,
+ 188,
+ 0,
+ 192,
+ 2,
+ "STRING"
+ ],
+ [
+ 485,
+ 188,
+ 0,
+ 193,
+ 2,
+ "STRING"
+ ],
+ [
+ 486,
+ 187,
+ 0,
+ 193,
+ 1,
+ "STRING"
+ ],
+ [
+ 487,
+ 193,
+ 0,
+ 194,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 488,
+ 192,
+ 0,
+ 195,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 489,
+ 194,
+ 4,
+ 195,
+ 4,
+ "LATENT"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_imagebash.json b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_imagebash.json
new file mode 100644
index 0000000000000000000000000000000000000000..d5cfe2e33fe79d581f5ba182bc130f7f8ff07765
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_imagebash.json
@@ -0,0 +1,1506 @@
+{
+ "last_node_id": 49,
+ "last_link_id": 78,
+ "nodes": [
+ {
+ "id": 22,
+ "type": "ttN pipeOUT",
+ "pos": [
+ 1180,
+ 30
+ ],
+ "size": {
+ "0": 210,
+ "1": 186
+ },
+ "flags": {
+ "collapsed": true
+ },
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 38
+ }
+ ],
+ "outputs": [
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "pos",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "neg",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": [
+ 43
+ ],
+ "shape": 3,
+ "slot_index": 5
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": [
+ 44
+ ],
+ "shape": 3,
+ "slot_index": 8
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeOUT",
+ "ttNbgOverride": "",
+ "ttNnodeVersion": "1.1.0"
+ },
+ "color": "#222",
+ "bgcolor": "#000"
+ },
+ {
+ "id": 20,
+ "type": "ttN pipeOUT",
+ "pos": [
+ 440,
+ 30
+ ],
+ "size": {
+ "0": 210,
+ "1": 186
+ },
+ "flags": {
+ "collapsed": true
+ },
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 75
+ }
+ ],
+ "outputs": [
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "pos",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "neg",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": [
+ 40
+ ],
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeOUT",
+ "ttNbgOverride": "",
+ "ttNnodeVersion": "1.1.0"
+ },
+ "color": "#222",
+ "bgcolor": "#000"
+ },
+ {
+ "id": 26,
+ "type": "Reroute",
+ "pos": [
+ 760,
+ 40
+ ],
+ "size": [
+ 82,
+ 26
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 74
+ }
+ ],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 39
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "showOutputText": true,
+ "horizontal": false
+ },
+ "color": "#2a363b",
+ "bgcolor": "#3f5159"
+ },
+ {
+ "id": 27,
+ "type": "Reroute",
+ "pos": [
+ 1180,
+ 40
+ ],
+ "size": [
+ 82,
+ 26
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 39
+ }
+ ],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 45
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "showOutputText": true,
+ "horizontal": false
+ },
+ "color": "#2a363b",
+ "bgcolor": "#3f5159"
+ },
+ {
+ "id": 46,
+ "type": "ttN pipeKSampler",
+ "pos": [
+ 2250,
+ 0
+ ],
+ "size": [
+ 680,
+ 930
+ ],
+ "flags": {},
+ "order": 16,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 69
+ },
+ {
+ "name": "optional_model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "optional_positive",
+ "type": "CONDITIONING",
+ "link": 68
+ },
+ {
+ "name": "optional_negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_latent",
+ "type": "LATENT",
+ "link": 67
+ },
+ {
+ "name": "optional_vae",
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ },
+ {
+ "name": "xyPlot",
+ "type": "XYPLOT",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeKSampler",
+ "ttNnodeVersion": "1.0.5"
+ },
+ "widgets_values": [
+ "add_detail.safetensors",
+ 1,
+ 1,
+ "None",
+ 2,
+ "disabled",
+ "Sample",
+ "24",
+ 8,
+ "euler_ancestral",
+ "karras",
+ 0.55,
+ "Save",
+ "Comfy",
+ 171098745667926,
+ "randomize"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 45,
+ "type": "ttN hiresfixScale",
+ "pos": [
+ 1920,
+ 360
+ ],
+ "size": [
+ 315,
+ 340
+ ],
+ "flags": {},
+ "order": 15,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 65
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "link": 66
+ }
+ ],
+ "outputs": [
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 67
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "infoWidgetHidden": false,
+ "Node name for S&R": "ttN hiresfixScale",
+ "ttNnodeVersion": "1.0.3"
+ },
+ "widgets_values": [
+ "DF2K_JPEG.pth",
+ "Rescale based on model upscale image size ⬇",
+ true,
+ "nearest-exact",
+ "by percentage",
+ 50,
+ 512,
+ 512,
+ 1024,
+ "disabled",
+ "Hide",
+ "ComfyUI",
+ true
+ ],
+ "color": "#233",
+ "bgcolor": "#355"
+ },
+ {
+ "id": 31,
+ "type": "BNK_CLIPTextEncodeAdvanced",
+ "pos": [
+ 1330,
+ 70
+ ],
+ "size": {
+ "0": 280,
+ "1": 150
+ },
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "link": 43
+ }
+ ],
+ "outputs": [
+ {
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 68
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "BNK_CLIPTextEncodeAdvanced"
+ },
+ "widgets_values": [
+ "(RAW photo:1.2), perfect composition, (masterpiece, 8k, absurdres, best quality, intricate), realistic, raytracing, dark forest, mystical, pathway, horse, wide shot, side on, full body",
+ "none",
+ "comfy++"
+ ],
+ "color": "#432",
+ "bgcolor": "#653"
+ },
+ {
+ "id": 32,
+ "type": "ttN pipeOUT",
+ "pos": [
+ 1650,
+ 30
+ ],
+ "size": {
+ "0": 140,
+ "1": 186
+ },
+ "flags": {
+ "collapsed": true
+ },
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 44
+ }
+ ],
+ "outputs": [
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "pos",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "neg",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": [
+ 66
+ ],
+ "shape": 3,
+ "slot_index": 4
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": [
+ 69
+ ],
+ "shape": 3,
+ "slot_index": 8
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeOUT",
+ "ttNbgOverride": "",
+ "ttNnodeVersion": "1.1.0"
+ },
+ "color": "#222",
+ "bgcolor": "#000"
+ },
+ {
+ "id": 34,
+ "type": "EmptyLatentImage",
+ "pos": [
+ 780,
+ 110
+ ],
+ "size": {
+ "0": 210,
+ "1": 106
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 70
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "EmptyLatentImage"
+ },
+ "widgets_values": [
+ 512,
+ 512,
+ 1
+ ],
+ "color": "#233",
+ "bgcolor": "#355"
+ },
+ {
+ "id": 21,
+ "type": "ttN pipeOUT",
+ "pos": [
+ 612,
+ 30
+ ],
+ "size": {
+ "0": 210,
+ "1": 186
+ },
+ "flags": {
+ "collapsed": true
+ },
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 40,
+ "slot_index": 0
+ }
+ ],
+ "outputs": [
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "pos",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "neg",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": [
+ 38,
+ 72
+ ],
+ "shape": 3,
+ "slot_index": 8
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeOUT",
+ "ttNbgOverride": "",
+ "ttNnodeVersion": "1.1.0"
+ },
+ "color": "#222",
+ "bgcolor": "#000"
+ },
+ {
+ "id": 43,
+ "type": "ttN imageOutput",
+ "pos": [
+ 1640,
+ 410
+ ],
+ "size": [
+ 254.0441087542972,
+ 286.4180727359376
+ ],
+ "flags": {},
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 61
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 65
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN imageOutput",
+ "ttNnodeVersion": "1.1.0"
+ },
+ "widgets_values": [
+ "Preview",
+ "T:\\AI\\ComfyNew\\ComfyUI\\output",
+ "Comfy",
+ 5,
+ "PNG",
+ "False",
+ "True"
+ ],
+ "color": "#232",
+ "bgcolor": "#353"
+ },
+ {
+ "id": 29,
+ "type": "Image Overlay",
+ "pos": [
+ 1330,
+ 410
+ ],
+ "size": [
+ 288.12748302112914,
+ 290
+ ],
+ "flags": {},
+ "order": 13,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "base_image",
+ "type": "IMAGE",
+ "link": 45
+ },
+ {
+ "name": "overlay_image",
+ "type": "IMAGE",
+ "link": 46
+ },
+ {
+ "name": "optional_mask",
+ "type": "MASK",
+ "link": 47
+ }
+ ],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 61
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Image Overlay"
+ },
+ "widgets_values": [
+ "Resize by rescale_factor",
+ "nearest-exact",
+ 0.7,
+ 512,
+ 512,
+ 270,
+ 140,
+ 0,
+ 0
+ ],
+ "color": "#232",
+ "bgcolor": "#353"
+ },
+ {
+ "id": 28,
+ "type": "ttN imageREMBG",
+ "pos": [
+ 1100,
+ 420
+ ],
+ "size": [
+ 210,
+ 279.4180727359376
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 77
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 46
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "links": [
+ 47
+ ],
+ "shape": 3,
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN imageREMBG",
+ "ttNnodeVersion": "1.0.0"
+ },
+ "widgets_values": [
+ "Preview",
+ "ComfyUI"
+ ],
+ "color": "#232",
+ "bgcolor": "#353"
+ },
+ {
+ "id": 48,
+ "type": "ttN pipeKSampler",
+ "pos": [
+ 410,
+ 270
+ ],
+ "size": [
+ 340,
+ 650
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 76
+ },
+ {
+ "name": "optional_model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "optional_positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_latent",
+ "type": "LATENT",
+ "link": null
+ },
+ {
+ "name": "optional_vae",
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ },
+ {
+ "name": "xyPlot",
+ "type": "XYPLOT",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 74
+ ],
+ "shape": 3,
+ "slot_index": 7
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeKSampler",
+ "ttNnodeVersion": "1.0.5"
+ },
+ "widgets_values": [
+ "None",
+ 1,
+ 1,
+ "None",
+ 2,
+ "disabled",
+ "Sample",
+ "24",
+ 8,
+ "ddim",
+ "karras",
+ 1,
+ "Save",
+ "Comfy",
+ 940490427037325,
+ "randomize"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 47,
+ "type": "ttN pipeKSampler",
+ "pos": [
+ 780,
+ 270
+ ],
+ "size": [
+ 302.3999938964844,
+ 660
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 72
+ },
+ {
+ "name": "optional_model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "optional_positive",
+ "type": "CONDITIONING",
+ "link": 71
+ },
+ {
+ "name": "optional_negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_latent",
+ "type": "LATENT",
+ "link": 70
+ },
+ {
+ "name": "optional_vae",
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ },
+ {
+ "name": "xyPlot",
+ "type": "XYPLOT",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 77
+ ],
+ "shape": 3,
+ "slot_index": 7
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeKSampler",
+ "ttNnodeVersion": "1.0.5"
+ },
+ "widgets_values": [
+ "None",
+ 1,
+ 1,
+ "None",
+ 2,
+ "disabled",
+ "Sample",
+ "24",
+ 8,
+ "euler_ancestral",
+ "karras",
+ 1,
+ "Save",
+ "Comfy",
+ 59660880884484,
+ "randomize"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 49,
+ "type": "ttN pipeLoader",
+ "pos": [
+ 20,
+ 30
+ ],
+ "size": [
+ 370,
+ 815
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": [
+ 75,
+ 76
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": [
+ 78
+ ],
+ "shape": 3,
+ "slot_index": 6
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeLoader",
+ "ttNnodeVersion": "1.0.3"
+ },
+ "widgets_values": [
+ "Good\\deliberate_v2.safetensors",
+ "vae-ft-mse-840000-ema-pruned.safetensors",
+ -1,
+ "None",
+ 1,
+ 1,
+ "None",
+ 1,
+ 1,
+ "None",
+ 1,
+ 1,
+ "(RAW photo:1.2), perfect composition, (masterpiece, 8k, absurdres, best quality, intricate), realistic, raytracing, dark forest, mystical, pathway",
+ "none",
+ "comfy++",
+ "(semi-realistic, cgi, 3d, render, sketch, cartoon, drawing, digital art, anime, manga:1.3), out of frame, cropped, cut off, poorly made, username, signature, watermark, unattractive, blurry, boring, sketch, lacklustre, repetitive, worst quality, low quality, jpeg artefacts, poorly lit, overexposed, underexposed, glitch, error, out of focus, amateur, (poorly drawn hands, poorly drawn face:1.2), deformed iris, deformed pupils, morbid, duplicate, mutilated, extra fingers, mutated hands, poorly drawn eyes, mutation, deformed, dehydrated, bad anatomy, bad proportions, extra limbs, cloned face, disfigured, gross proportions, malformed limbs, missing arms, missing legs, extra arms, extra legs, fused fingers, too many fingers, long neck, incoherent, cloned face, cloned body, blur, blurry,(nsfw),(naked),(nude)",
+ "none",
+ "comfy++",
+ 768,
+ 512,
+ 1,
+ 302086422941626,
+ "fixed"
+ ],
+ "color": "#222",
+ "bgcolor": "#000"
+ },
+ {
+ "id": 23,
+ "type": "BNK_CLIPTextEncodeAdvanced",
+ "pos": [
+ 440,
+ 80
+ ],
+ "size": {
+ "0": 280,
+ "1": 150
+ },
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "link": 78
+ }
+ ],
+ "outputs": [
+ {
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 71
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "BNK_CLIPTextEncodeAdvanced"
+ },
+ "widgets_values": [
+ "(RAW photo:1.2), perfect composition, (masterpiece, 8k, absurdres, best quality, intricate), realistic, raytracing, horse, wide shot, side on, full body",
+ "none",
+ "comfy++"
+ ],
+ "color": "#432",
+ "bgcolor": "#653"
+ }
+ ],
+ "links": [
+ [
+ 38,
+ 21,
+ 8,
+ 22,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 39,
+ 26,
+ 0,
+ 27,
+ 0,
+ "*"
+ ],
+ [
+ 40,
+ 20,
+ 8,
+ 21,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 43,
+ 22,
+ 5,
+ 31,
+ 0,
+ "CLIP"
+ ],
+ [
+ 44,
+ 22,
+ 8,
+ 32,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 45,
+ 27,
+ 0,
+ 29,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 46,
+ 28,
+ 0,
+ 29,
+ 1,
+ "IMAGE"
+ ],
+ [
+ 47,
+ 28,
+ 1,
+ 29,
+ 2,
+ "MASK"
+ ],
+ [
+ 61,
+ 29,
+ 0,
+ 43,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 65,
+ 43,
+ 0,
+ 45,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 66,
+ 32,
+ 4,
+ 45,
+ 1,
+ "VAE"
+ ],
+ [
+ 67,
+ 45,
+ 0,
+ 46,
+ 4,
+ "LATENT"
+ ],
+ [
+ 68,
+ 31,
+ 0,
+ 46,
+ 2,
+ "CONDITIONING"
+ ],
+ [
+ 69,
+ 32,
+ 8,
+ 46,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 70,
+ 34,
+ 0,
+ 47,
+ 4,
+ "LATENT"
+ ],
+ [
+ 71,
+ 23,
+ 0,
+ 47,
+ 2,
+ "CONDITIONING"
+ ],
+ [
+ 72,
+ 21,
+ 8,
+ 47,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 74,
+ 48,
+ 7,
+ 26,
+ 0,
+ "*"
+ ],
+ [
+ 75,
+ 49,
+ 0,
+ 20,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 76,
+ 49,
+ 0,
+ 48,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 77,
+ 47,
+ 7,
+ 28,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 78,
+ 49,
+ 6,
+ 23,
+ 0,
+ "CLIP"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_imagebash.png b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_imagebash.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb872a9305fc9124b5b28451ee78834de2162cc0
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_imagebash.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5a4385dbc54105815926da607c6938ba386fc8a98a445345e62d1fc5d08f292c
+size 1042367
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_pipeSDXL.json b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_pipeSDXL.json
new file mode 100644
index 0000000000000000000000000000000000000000..84d88c45aba027eda3cf136843846d2d9fb4996f
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_pipeSDXL.json
@@ -0,0 +1,327 @@
+{
+ "last_node_id": 195,
+ "last_link_id": 478,
+ "nodes": [
+ {
+ "id": 195,
+ "type": "ttN pipeKSamplerSDXL",
+ "pos": [
+ 520,
+ 40
+ ],
+ "size": [
+ 400,
+ 850
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "sdxl_pipe",
+ "type": "PIPE_LINE_SDXL",
+ "link": 478
+ },
+ {
+ "name": "optional_model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "optional_positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_vae",
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "optional_refiner_model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "optional_refiner_positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_refiner_negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_refiner_vae",
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "optional_latent",
+ "type": "LATENT",
+ "link": null
+ },
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "sdxl_pipe",
+ "type": "PIPE_LINE_SDXL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "refiner_model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "refiner_positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "refiner_negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "refiner_vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeKSamplerSDXL",
+ "ttNnodeVersion": "1.0.2"
+ },
+ "widgets_values": [
+ "None",
+ 2,
+ "disabled",
+ "Sample",
+ 20,
+ 5,
+ 8,
+ "euler",
+ "normal",
+ "Preview",
+ "ComfyUI",
+ 774522948921345,
+ "randomize"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 194,
+ "type": "ttN pipeLoaderSDXL",
+ "pos": [
+ 10,
+ 40
+ ],
+ "size": [
+ 480,
+ 850
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "sdxl_pipe",
+ "type": "PIPE_LINE_SDXL",
+ "links": [
+ 478
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "refiner_model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "refiner_positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "refiner_negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "refiner_vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "refiner_clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeLoaderSDXL",
+ "ttNnodeVersion": "1.1.1"
+ },
+ "widgets_values": [
+ "sd_xl_base_1.0.safetensors",
+ "Baked VAE",
+ "None",
+ 1,
+ 1,
+ "None",
+ 1,
+ 1,
+ "sd_xl_refiner_1.0.safetensors",
+ "Baked VAE",
+ "None",
+ 1,
+ 1,
+ "None",
+ 1,
+ 1,
+ -2,
+ "evening sunset scenery blue sky nature, glass bottle with a galaxy in it",
+ "none",
+ "comfy",
+ "text, watermark",
+ "none",
+ "comfy",
+ 1024,
+ 1024,
+ 1,
+ 721897303308196,
+ "fixed"
+ ],
+ "color": "#222",
+ "bgcolor": "#000"
+ }
+ ],
+ "links": [
+ [
+ 478,
+ 194,
+ 0,
+ 195,
+ 0,
+ "PIPE_LINE_SDXL"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_pipeSDXL.png b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_pipeSDXL.png
new file mode 100644
index 0000000000000000000000000000000000000000..09a49b1059de1b92539d8f1f11f5243c6bfb9d50
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_pipeSDXL.png differ
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_prefixParsing.png b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_prefixParsing.png
new file mode 100644
index 0000000000000000000000000000000000000000..310ac6f54145dcf485cfacbcb0e039ee87d6d603
Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_prefixParsing.png differ
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_trueHRFix.json b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_trueHRFix.json
new file mode 100644
index 0000000000000000000000000000000000000000..20b1c16b872d35a04f819d153ebd25f270088fb0
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_trueHRFix.json
@@ -0,0 +1,507 @@
+{
+ "last_node_id": 25,
+ "last_link_id": 16,
+ "nodes": [
+ {
+ "id": 25,
+ "type": "ttN pipeKSampler",
+ "pos": [
+ 1200,
+ 40
+ ],
+ "size": [
+ 500,
+ 800
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 13
+ },
+ {
+ "name": "optional_model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "optional_positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_latent",
+ "type": "LATENT",
+ "link": 16
+ },
+ {
+ "name": "optional_vae",
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ },
+ {
+ "name": "xyPlot",
+ "type": "XYPLOT",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeKSampler",
+ "ttNnodeVersion": "1.0.5"
+ },
+ "widgets_values": [
+ "add_detail.safetensors",
+ 1,
+ 1,
+ "None",
+ 2,
+ "disabled",
+ "Sample",
+ 24,
+ "7.000",
+ "dpmpp_2m",
+ "karras",
+ 0.5,
+ "Save",
+ "Comfy",
+ 996304000155359,
+ "randomize"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 22,
+ "type": "ttN pipeLoader",
+ "pos": [
+ 10,
+ 40
+ ],
+ "size": [
+ 400,
+ 720
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": [
+ 12
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeLoader",
+ "ttNnodeVersion": "1.0.3"
+ },
+ "widgets_values": [
+ "dreamshaper_6NoVae.safetensors",
+ "vae-ft-mse-840000-ema-pruned.safetensors",
+ -1,
+ "LowRA.safetensors",
+ 0.2,
+ 0,
+ "None",
+ 1,
+ 1,
+ "None",
+ 1,
+ 1,
+ "warm sunrays, mountains in the background, lush green plants, hobbit city (RAW photo:1.2), perfect composition, (masterpiece, 8k, absurdres, best quality, intricate), realistic, raytracing, sharp,",
+ "none",
+ "comfy++",
+ "disconnected, merged, weird, ugly, text, signature, watermark, blurry, blurred",
+ "none",
+ "comfy++",
+ 768,
+ 512,
+ 1,
+ 278267302676039,
+ "fixed"
+ ],
+ "color": "#222",
+ "bgcolor": "#000"
+ },
+ {
+ "id": 23,
+ "type": "ttN pipeKSampler",
+ "pos": [
+ 430,
+ 40
+ ],
+ "size": [
+ 350,
+ 630
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 12
+ },
+ {
+ "name": "optional_model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "optional_positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_latent",
+ "type": "LATENT",
+ "link": null
+ },
+ {
+ "name": "optional_vae",
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ },
+ {
+ "name": "xyPlot",
+ "type": "XYPLOT",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": [
+ 13
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": [
+ 15
+ ],
+ "shape": 3,
+ "slot_index": 5
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 14
+ ],
+ "shape": 3,
+ "slot_index": 7
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeKSampler",
+ "ttNnodeVersion": "1.0.5"
+ },
+ "widgets_values": [
+ "None",
+ 1,
+ 1,
+ "None",
+ 2,
+ "disabled",
+ "Sample",
+ 24,
+ "7.000",
+ "dpmpp_2m",
+ "karras",
+ 1,
+ "Preview",
+ "Comfy",
+ 641647281324617,
+ "randomize"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 24,
+ "type": "ttN hiresfixScale",
+ "pos": [
+ 800,
+ 140
+ ],
+ "size": [
+ 380,
+ 530
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 14
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "link": 15
+ }
+ ],
+ "outputs": [
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 16
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "infoWidgetHidden": false,
+ "Node name for S&R": "ttN hiresfixScale",
+ "ttNnodeVersion": "1.0.3"
+ },
+ "widgets_values": [
+ "DF2K_JPEG.pth",
+ "Rescale based on model upscale image size ⬇",
+ true,
+ "bilinear",
+ "by percentage",
+ 50,
+ 512,
+ 512,
+ 1024,
+ "disabled",
+ "Preview",
+ "ComfyUI",
+ true
+ ],
+ "color": "#233",
+ "bgcolor": "#355"
+ }
+ ],
+ "links": [
+ [
+ 12,
+ 22,
+ 0,
+ 23,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 13,
+ 23,
+ 0,
+ 25,
+ 0,
+ "PIPE_LINE"
+ ],
+ [
+ 14,
+ 23,
+ 7,
+ 24,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 15,
+ 23,
+ 5,
+ 24,
+ 1,
+ "VAE"
+ ],
+ [
+ 16,
+ 24,
+ 0,
+ 25,
+ 4,
+ "LATENT"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_trueHRFix.png b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_trueHRFix.png
new file mode 100644
index 0000000000000000000000000000000000000000..241f0eef9617a60bc08e9b29253036eaf1f394ae
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_trueHRFix.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8af85afd37064ad9c8b6115273f818c0671df629e732f43ba99e147ccff21cc6
+size 1237098
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_xyPlot.json b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_xyPlot.json
new file mode 100644
index 0000000000000000000000000000000000000000..c0f91269c6275a6a3ab97de25f648078e9b66fb0
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_xyPlot.json
@@ -0,0 +1,313 @@
+{
+ "last_node_id": 27,
+ "last_link_id": 19,
+ "nodes": [
+ {
+ "id": 26,
+ "type": "ttN pipeKSampler",
+ "pos": [
+ 1000,
+ 70
+ ],
+ "size": [
+ 720,
+ 1080
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "link": 19
+ },
+ {
+ "name": "optional_model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "optional_positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "optional_latent",
+ "type": "LATENT",
+ "link": null
+ },
+ {
+ "name": "optional_vae",
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ },
+ {
+ "name": "xyPlot",
+ "type": "XYPLOT",
+ "link": 18
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeKSampler",
+ "ttNnodeVersion": "1.0.5"
+ },
+ "widgets_values": [
+ "None",
+ 1,
+ 1,
+ "None",
+ 2,
+ "disabled",
+ "Sample",
+ 24,
+ "7.000",
+ "dpmpp_2m",
+ "karras",
+ 1,
+ "Save",
+ "Comfy",
+ 861410848433374,
+ "randomize"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 24,
+ "type": "ttN xyPlot",
+ "pos": [
+ 650,
+ 210
+ ],
+ "size": {
+ "0": 300,
+ "1": 300
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "xyPlot",
+ "type": "XYPLOT",
+ "links": [
+ 18
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN xyPlot",
+ "ttNnodeVersion": "1.2.0"
+ },
+ "widgets_values": [
+ 0,
+ 0,
+ "False",
+ "False",
+ "loader: ckpt_name",
+ "Good\\deliberate_v2.safetensors; dreamshaper_6NoVae.safetensors; landscapePhotoreal_v1.safetensors; ",
+ "sampler: seed",
+ "increment; increment; increment; increment; "
+ ],
+ "color": "#233",
+ "bgcolor": "#355"
+ },
+ {
+ "id": 27,
+ "type": "ttN pipeLoader",
+ "pos": [
+ 200,
+ 70
+ ],
+ "size": [
+ 400,
+ 740
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "optional_clip",
+ "type": "CLIP",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "PIPE_LINE",
+ "links": [
+ 19
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "model",
+ "type": "MODEL",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "latent",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ttN pipeLoader",
+ "ttNnodeVersion": "1.0.3"
+ },
+ "widgets_values": [
+ "dreamshaper_6NoVae.safetensors",
+ "vae-ft-mse-840000-ema-pruned.safetensors",
+ -1,
+ "None",
+ 0.2,
+ 0.2,
+ "None",
+ 1,
+ 1,
+ "None",
+ 1,
+ 1,
+ "warm sunrays, mountains in the background, lush green plants, hobbit city (RAW photo:1.2), perfect composition, (masterpiece, 8k, absurdres, best quality, intricate), realistic, raytracing, sharp,",
+ "none",
+ "comfy++",
+ "disconnected, merged, weird, ugly, text, signature, watermark, blurry, blurred",
+ "none",
+ "comfy++",
+ 768,
+ 512,
+ 1,
+ 278267302676039,
+ "fixed"
+ ],
+ "color": "#222",
+ "bgcolor": "#000"
+ }
+ ],
+ "links": [
+ [
+ 18,
+ 24,
+ 0,
+ 26,
+ 7,
+ "XYPLOT"
+ ],
+ [
+ 19,
+ 27,
+ 0,
+ 26,
+ 0,
+ "PIPE_LINE"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_xyPlot.png b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_xyPlot.png
new file mode 100644
index 0000000000000000000000000000000000000000..8bb4f6a1aec0993ff34a14862a366b7e6d6ab1d8
--- /dev/null
+++ b/ComfyUI/custom_nodes/ComfyUI_tinyterraNodes/workflows/tinyterra_xyPlot.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6d0ec5d70747a3d6084f67ab36eeb5be3330f75c83d89e3ab9f13abe9962b4d7
+size 1448860
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/.github/FUNDING.yml b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/.github/FUNDING.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d635db616cfdf279730713380050e6805be12a40
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/.github/FUNDING.yml
@@ -0,0 +1,13 @@
+# These are supported funding model platforms
+
+github: [jags111]
+patreon: # jags111
+open_collective: # Replace with a single Open Collective username
+ko_fi: # B0B7IHIO1
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
+custom: ['https://www.buymeacoffee.com/jagsai','https://www. paypal.me/revsmart", orchidsasia.com' ]
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/LICENSE b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..f288702d2fa16d3cdf0035b15a9fcbc552cd88e7
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/README.md b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..829f45d58600fde8fe9302e3d6ab3c9f90ab7e38
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/README.md
@@ -0,0 +1,251 @@
+✨🍬Planning to help this branch stay alive and any issues will try to solve or fix .. But will be slow as I run many github repos . before raising any issues, please update comfyUI to the latest and esnure all the required packages are updated ass well. Share your workflow in issues to retest same at our end and update the patch.🍬
+
+
+ Efficiency Nodes for ComfyUI Version 2.0+
+=======
+### A collection of ComfyUI custom nodes to help streamline workflows and reduce total node count.
+## Releases
+
+Please check out our WIKI for any use cases and new developments including workflow and settings.
+[Efficiency Nodes Wiki](https://github.com/jags111/efficiency-nodes-comfyui/wiki)
+
+### Nodes:
+
+
+ Efficient Loader & Eff. Loader SDXL
+
+
Nodes that can load & cache Checkpoint, VAE, & LoRA type models. (cache settings found in config file 'node_settings.json')
+
Able to apply LoRA & Control Net stacks via their lora_stack and cnet_stack inputs.
+
Come with positive and negative prompt text boxes. You can also set the way you want the prompt to be encoded via the token_normalization and weight_interpretation widgets.
+
These node's also feature a variety of custom menu options as shown below.
+
+
note: "🔍 View model info..." requires ComfyUI-Custom-Scripts to be installed to function.
+
These loaders are used by the XY Plot node for many of its plot type dependencies.
+
+
+
+
+
+
+
+
+
+ KSampler (Efficient), KSampler Adv. (Efficient), KSampler SDXL (Eff.)
+
+- Modded KSamplers with the ability to live preview generations and/or vae decode images.
+- Feature a special seed box that allows for a clearer management of seeds. (-1 seed to apply the selected seed behavior)
+- Can execute a variety of scripts, such as the XY Plot script. To activate the script, simply connect the input connection.
+
+
+
+
+
+
+
+
+
+
+
+
+ Script Nodes
+
+- A group of node's that are used in conjuction with the Efficient KSamplers to execute a variety of 'pre-wired' set of actions.
+- Script nodes can be chained if their input/outputs allow it. Multiple instances of the same Script Node in a chain does nothing.
+
+
+
+
+
+ XY Plot
+
+
Node that allows users to specify parameters for the Efficiency KSamplers to plot on a grid.
+
+
+
+
+
+
+
+
+ HighRes-Fix
+
+
Node that the gives user the ability to upscale KSampler results through variety of different methods.
Supports ControlNet guided latent upscaling. (You must have Fannovel's comfyui_controlnet_aux installed to unlock this feature)
+
Local models---The node pulls the required files from huggingface hub by default. You can create a models folder and place the modules there if you have a flaky connection or prefer to use it completely offline, it will load them locally instead. The path should be: ComfyUI/custom_nodes/efficiency-nodes-comfyui/models; Alternatively, just clone the entire HF repo to it: (git clone https://huggingface.co/city96/SD-Latent-Upscaler) to ComfyUI/custom_nodes/efficiency-nodes-comfyui/models
+
+
+
+
+
+
+
+
+ Noise Control
+
+
This node gives the user the ability to manipulate noise sources in a variety of ways, such as the sampling's RNG source.
+
The CFG Denoiser noise hijack was developed by smZ, it allows you to get closer recreating Automatic1111 results.
+ Note: The CFG Denoiser does not work with a variety of conditioning types such as ControlNet & GLIGEN
+
This node also allows you to add noise Seed Variations to your generations.
+
For trying to replicate Automatic1111 images, this node will help you achieve it. Encode your prompt using "length+mean" token_normalization with "A1111" weight_interpretation, set the Noise Control Script node's rng_source to "gpu", and turn the cfg_denoiser to true.
+
+
+
+
+
+
+
+
+ Tiled Upscaler
+
+
The Tiled Upscaler script attempts to encompas BlenderNeko's ComfyUI_TiledKSampler workflow into 1 node.
+
Script supports Tiled ControlNet help via the options.
+
Strongly recommend the preview_method be "vae_decoded_only" when running the script.
+
+
+
+
+
+
+
+
+ AnimateDiff
+
+
To unlock the AnimateDiff script it is required you have installed Kosinkadink's ComfyUI-AnimateDiff-Evolved.
+
The latent batch_size when running this script becomes your frame count.
+
+
+
+
+
+
+
+
+
+
+ Image Overlay
+
+
Node that allows for flexible image overlaying. Works also with image batches.
+
+
+
+
+
+
+
+
+ SimpleEval Nodes
+
+
A collection of nodes that allows users to write simple Python expressions for a variety of data types using the simpleeval library.
+
To activate you must have installed the simpleeval library in your Python workspace.
+
pip install simpleeval
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Latent Upscale nodes
+
+
Forked from NN latent this node provides some remarkable neural enhancement to the latents making scaling a cool task
+
Both NN latent upscale and Latent upscaler does the Latent improvemnet in remarkable ways. If you face any issue regarding same please install the nodes from this link([SD-Latent-Upscaler](https://github.com/city96/SD-Latent-Upscaler) and the NN latent upscale from [ComfyUI_NNlatentUpscale](https://github.com/Ttl/ComfyUi_NNLatentUpscale)
+
+
+
+
+
+
+
+
+
+
+
+
+## Workflow Examples:
+
+Kindly load all PNG files in same name in the (workflow driectory) to comfyUI to get all this workflows. The PNG files have the json embedded into them and are easy to drag and drop !
+
+1. HiRes-Fixing
+ [](https://github.com/jags111/efficiency-nodes-comfyui/blob/main/workflows/HiResfix_workflow.png)
+
+2. SDXL Refining & **Noise Control Script**
+ [](https://github.com/jags111/efficiency-nodes-comfyui/blob/main/workflows/SDXL_base_refine_noise_workflow.png)
+
+3. **XY Plot**: LoRA model_strength vs clip_strength
+ [](https://github.com/jags111/efficiency-nodes-comfyui/blob/main/workflows/Eff_XYPlot%20-%20LoRA%20Model%20vs%20Clip%20Strengths01.png)
+
+4. Stacking Scripts: **XY Plot** + **Noise Control** + **HiRes-Fix**
+ [](https://github.com/LucianoCirino/efficiency-nodes-comfyui/blob/v2.0/workflows/XYPlot%20-%20Seeds%20vs%20Checkpoints%20%26%20Stacked%20Scripts.png)
+
+5. Stacking Scripts: **AnimateDiff** + **HiRes-Fix** (with ControlNet)
+ [](https://github.com/jags111/efficiency-nodes-comfyui/blob/main/workflows/eff_animatescriptWF001.gif)
+
+6. SVD workflow: **Stable Video Diffusion** + *Kohya Hires** (with latent control)
+
+
+
+### Dependencies
+The python library simpleeval is required to be installed if you wish to use the **Simpleeval Nodes**.
+
pip install simpleeval
+Also can be installed with a simple pip command
+'pip install simpleeval'
+
+A single file library for easily adding evaluatable expressions into python projects. Say you want to allow a user to set an alarm volume, which could depend on the time of day, alarm level, how many previous alarms had gone off, and if there is music playing at the time.
+
+check Notes for more information.
+
+## **Install:**
+To install, drop the "_**efficiency-nodes-comfyui**_" folder into the "_**...\ComfyUI\ComfyUI\custom_nodes**_" directory and restart UI.
+
+## Todo
+
+[ ] Add guidance to notebook
+
+
+# Comfy Resources
+
+**Efficiency Linked Repos**
+- [BlenderNeko ComfyUI_ADV_CLIP_emb](https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb) by@BlenderNeko
+- [Chrisgoringe cg-noise](https://github.com/chrisgoringe/cg-noise) by@Chrisgoringe
+- [pythongosssss ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) by@pythongosssss
+- [shiimizu ComfyUI_smZNodes](https://github.com/shiimizu/ComfyUI_smZNodes) by@shiimizu
+- [LEv145_images-grid-comfyUI-plugin](https://github.com/LEv145/images-grid-comfy-plugin)) by@LEv145
+- [ltdrdata-ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) by@ltdrdata
+- [pythongosssss-ComfyUI-custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) by@pythongosssss
+- [RockOfFire-ComfyUI_Comfyroll_CustomNodes](https://github.com/RockOfFire/ComfyUI_Comfyroll_CustomNodes) by@RockOfFire
+
+**Guides**:
+- [Official Examples (eng)](https://comfyanonymous.github.io/ComfyUI_examples/)-
+- [ComfyUI Community Manual (eng)](https://blenderneko.github.io/ComfyUI-docs/) by @BlenderNeko
+
+- **Extensions and Custom Nodes**:
+- [Plugins for Comfy List (eng)](https://github.com/WASasquatch/comfyui-plugins) by @WASasquatch
+- [ComfyUI tag on CivitAI (eng)](https://civitai.com/tag/comfyui)-
+- [Tomoaki's personal Wiki (jap)](https://comfyui.creamlab.net/guides/) by @tjhayasaka
+
+ ## Support
+If you create a cool image with our nodes, please show your result and message us on twitter at @jags111 or @NeuralismAI .
+
+You can join the NEURALISM AI DISCORD or JAGS AI DISCORD
+Share your work created with this model. Exchange experiences and parameters. And see more interesting custom workflows.
+
+Support us in Patreon for more future models and new versions of AI notebooks.
+- tip me on [patreon]
+
+ My buymeacoffee.com pages and links are here and if you feel you are happy with my work just buy me a coffee !
+
+ coffee for JAGS AI
+
+Thank you for being awesome!
+
+
+
+
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__init__.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a73a32cee3bfc5a6015a3da0510126cc117b48d7
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__init__.py
@@ -0,0 +1,18 @@
+import os
+import subprocess
+import importlib.util
+import folder_paths
+import shutil
+import sys
+import traceback
+
+from .efficiency_nodes import NODE_CLASS_MAPPINGS
+#from .py.ttl_nn_latent_upscaler import NODE_CLASS_MAPPINGS
+#from .py.city96_latent_upscaler import NODE_CLASS_MAPPINGS
+
+
+WEB_DIRECTORY = "js"
+
+CC_VERSION = 2.0
+
+__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'CC_VERSION']
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__pycache__/__init__.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3708210bbe29762a9c0ac9913e4b959034d54e2d
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__pycache__/__init__.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__pycache__/efficiency_nodes.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__pycache__/efficiency_nodes.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..60fb916bf6aa4f49cbed2d8e8d549f1319315f39
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__pycache__/efficiency_nodes.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__pycache__/tsc_utils.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__pycache__/tsc_utils.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d5db758de5b0fa1f98c76378b66fa96616601e5b
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/__pycache__/tsc_utils.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/arial.ttf b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/arial.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..ff0815cd8c64b0a245ec780eb8d21867509155b5
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/arial.ttf differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/efficiency_nodes.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/efficiency_nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..39e014f55543424a57d5d86360ba1e16c3892f35
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/efficiency_nodes.py
@@ -0,0 +1,4350 @@
+# Efficiency Nodes - A collection of my ComfyUI custom nodes to help streamline workflows and reduce total node count.
+# by Luciano Cirino (Discord: TSC#9184) - April 2023 - October 2023
+# https://github.com/LucianoCirino/efficiency-nodes-comfyui
+
+from torch import Tensor
+from PIL import Image, ImageOps, ImageDraw, ImageFont
+from PIL.PngImagePlugin import PngInfo
+import numpy as np
+import torch
+
+import ast
+from pathlib import Path
+from importlib import import_module
+import os
+import sys
+import copy
+import subprocess
+import json
+import psutil
+
+# Get the absolute path of various directories
+my_dir = os.path.dirname(os.path.abspath(__file__))
+custom_nodes_dir = os.path.abspath(os.path.join(my_dir, '..'))
+comfy_dir = os.path.abspath(os.path.join(my_dir, '..', '..'))
+
+# Construct the path to the font file
+font_path = os.path.join(my_dir, 'arial.ttf')
+
+# Append comfy_dir to sys.path & import files
+sys.path.append(comfy_dir)
+from nodes import LatentUpscaleBy, KSampler, KSamplerAdvanced, VAEDecode, VAEDecodeTiled, VAEEncode, VAEEncodeTiled, \
+ ImageScaleBy, CLIPSetLastLayer, CLIPTextEncode, ControlNetLoader, ControlNetApply, ControlNetApplyAdvanced, \
+ PreviewImage, MAX_RESOLUTION
+from comfy_extras.nodes_upscale_model import UpscaleModelLoader, ImageUpscaleWithModel
+from comfy_extras.nodes_clip_sdxl import CLIPTextEncodeSDXL, CLIPTextEncodeSDXLRefiner
+import comfy.sample
+import comfy.samplers
+import comfy.sd
+import comfy.utils
+import comfy.latent_formats
+sys.path.remove(comfy_dir)
+
+# Append my_dir to sys.path & import files
+sys.path.append(my_dir)
+from tsc_utils import *
+from .py import smZ_cfg_denoiser
+from .py import smZ_rng_source
+from .py import cg_mixed_seed_noise
+from .py import city96_latent_upscaler
+from .py import ttl_nn_latent_upscaler
+from .py import bnk_tiled_samplers
+from .py import bnk_adv_encode
+sys.path.remove(my_dir)
+
+# Append custom_nodes_dir to sys.path
+sys.path.append(custom_nodes_dir)
+
+# GLOBALS
+REFINER_CFG_OFFSET = 0 #Refiner CFG Offset
+
+########################################################################################################################
+# Common function for encoding prompts
+def encode_prompts(positive_prompt, negative_prompt, token_normalization, weight_interpretation, clip, clip_skip,
+ refiner_clip, refiner_clip_skip, ascore, is_sdxl, empty_latent_width, empty_latent_height,
+ return_type="both"):
+
+ positive_encoded = negative_encoded = refiner_positive_encoded = refiner_negative_encoded = None
+
+ # Process base encodings if needed
+ if return_type in ["base", "both"]:
+ clip = CLIPSetLastLayer().set_last_layer(clip, clip_skip)[0]
+
+ positive_encoded = bnk_adv_encode.AdvancedCLIPTextEncode().encode(clip, positive_prompt, token_normalization, weight_interpretation)[0]
+ negative_encoded = bnk_adv_encode.AdvancedCLIPTextEncode().encode(clip, negative_prompt, token_normalization, weight_interpretation)[0]
+
+ # Process refiner encodings if needed
+ if return_type in ["refiner", "both"] and is_sdxl and refiner_clip and refiner_clip_skip and ascore:
+ refiner_clip = CLIPSetLastLayer().set_last_layer(refiner_clip, refiner_clip_skip)[0]
+
+ refiner_positive_encoded = bnk_adv_encode.AdvancedCLIPTextEncode().encode(refiner_clip, positive_prompt, token_normalization, weight_interpretation)[0]
+ refiner_positive_encoded = bnk_adv_encode.AddCLIPSDXLRParams().encode(refiner_positive_encoded, empty_latent_width, empty_latent_height, ascore[0])[0]
+
+ refiner_negative_encoded = bnk_adv_encode.AdvancedCLIPTextEncode().encode(refiner_clip, negative_prompt, token_normalization, weight_interpretation)[0]
+ refiner_negative_encoded = bnk_adv_encode.AddCLIPSDXLRParams().encode(refiner_negative_encoded, empty_latent_width, empty_latent_height, ascore[1])[0]
+
+ # Return results based on return_type
+ if return_type == "base":
+ return positive_encoded, negative_encoded, clip
+ elif return_type == "refiner":
+ return refiner_positive_encoded, refiner_negative_encoded, refiner_clip
+ elif return_type == "both":
+ return positive_encoded, negative_encoded, clip, refiner_positive_encoded, refiner_negative_encoded, refiner_clip
+
+########################################################################################################################
+# TSC Efficient Loader
+class TSC_EfficientLoader:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": { "ckpt_name": (folder_paths.get_filename_list("checkpoints"),),
+ "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
+ "clip_skip": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}),
+ "lora_name": (["None"] + folder_paths.get_filename_list("loras"),),
+ "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "positive": ("STRING", {"default": "CLIP_POSITIVE","multiline": True}),
+ "negative": ("STRING", {"default": "CLIP_NEGATIVE", "multiline": True}),
+ "token_normalization": (["none", "mean", "length", "length+mean"],),
+ "weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"],),
+ "empty_latent_width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
+ "empty_latent_height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
+ "batch_size": ("INT", {"default": 1, "min": 1, "max": 262144})},
+ "optional": {"lora_stack": ("LORA_STACK", ),
+ "cnet_stack": ("CONTROL_NET_STACK",)},
+ "hidden": { "prompt": "PROMPT",
+ "my_unique_id": "UNIQUE_ID",},
+ }
+
+ RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "DEPENDENCIES",)
+ RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", "CLIP", "DEPENDENCIES", )
+ FUNCTION = "efficientloader"
+ CATEGORY = "Efficiency Nodes/Loaders"
+
+ def efficientloader(self, ckpt_name, vae_name, clip_skip, lora_name, lora_model_strength, lora_clip_strength,
+ positive, negative, token_normalization, weight_interpretation, empty_latent_width,
+ empty_latent_height, batch_size, lora_stack=None, cnet_stack=None, refiner_name="None",
+ ascore=None, prompt=None, my_unique_id=None, loader_type="regular"):
+
+ # Clean globally stored objects
+ globals_cleanup(prompt)
+
+ # Create Empty Latent
+ latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8]).cpu()
+
+ # Retrieve cache numbers
+ vae_cache, ckpt_cache, lora_cache, refn_cache = get_cache_numbers("Efficient Loader")
+
+ if lora_name != "None" or lora_stack:
+ # Initialize an empty list to store LoRa parameters.
+ lora_params = []
+
+ # Check if lora_name is not the string "None" and if so, add its parameters.
+ if lora_name != "None":
+ lora_params.append((lora_name, lora_model_strength, lora_clip_strength))
+
+ # If lora_stack is not None or an empty list, extend lora_params with its items.
+ if lora_stack:
+ lora_params.extend(lora_stack)
+
+ # Load LoRa(s)
+ model, clip = load_lora(lora_params, ckpt_name, my_unique_id, cache=lora_cache, ckpt_cache=ckpt_cache, cache_overwrite=True)
+
+ if vae_name == "Baked VAE":
+ vae = get_bvae_by_ckpt_name(ckpt_name)
+ else:
+ model, clip, vae = load_checkpoint(ckpt_name, my_unique_id, cache=ckpt_cache, cache_overwrite=True)
+ lora_params = None
+
+ # Load Refiner Checkpoint if given
+ if refiner_name != "None":
+ refiner_model, refiner_clip, _ = load_checkpoint(refiner_name, my_unique_id, output_vae=False,
+ cache=refn_cache, cache_overwrite=True, ckpt_type="refn")
+ else:
+ refiner_model = refiner_clip = None
+
+ # Extract clip_skips
+ refiner_clip_skip = clip_skip[1] if loader_type == "sdxl" else None
+ clip_skip = clip_skip[0] if loader_type == "sdxl" else clip_skip
+
+ # Encode prompt based on loader_type
+ positive_encoded, negative_encoded, clip, refiner_positive_encoded, refiner_negative_encoded, refiner_clip = \
+ encode_prompts(positive, negative, token_normalization, weight_interpretation, clip, clip_skip,
+ refiner_clip, refiner_clip_skip, ascore, loader_type == "sdxl",
+ empty_latent_width, empty_latent_height)
+
+ # Apply ControlNet Stack if given
+ if cnet_stack:
+ controlnet_conditioning = TSC_Apply_ControlNet_Stack().apply_cnet_stack(positive_encoded, negative_encoded, cnet_stack)
+ positive_encoded, negative_encoded = controlnet_conditioning[0], controlnet_conditioning[1]
+
+ # Check for custom VAE
+ if vae_name != "Baked VAE":
+ vae = load_vae(vae_name, my_unique_id, cache=vae_cache, cache_overwrite=True)
+
+ # Data for XY Plot
+ dependencies = (vae_name, ckpt_name, clip, clip_skip, refiner_name, refiner_clip, refiner_clip_skip,
+ positive, negative, token_normalization, weight_interpretation, ascore,
+ empty_latent_width, empty_latent_height, lora_params, cnet_stack)
+
+ ### Debugging
+ ###print_loaded_objects_entries()
+ print_loaded_objects_entries(my_unique_id, prompt)
+
+ if loader_type == "regular":
+ return (model, positive_encoded, negative_encoded, {"samples":latent}, vae, clip, dependencies,)
+ elif loader_type == "sdxl":
+ return ((model, clip, positive_encoded, negative_encoded, refiner_model, refiner_clip,
+ refiner_positive_encoded, refiner_negative_encoded), {"samples":latent}, vae, dependencies,)
+
+#=======================================================================================================================
+# TSC Efficient Loader SDXL
+class TSC_EfficientLoaderSDXL(TSC_EfficientLoader):
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": { "base_ckpt_name": (folder_paths.get_filename_list("checkpoints"),),
+ "base_clip_skip": ("INT", {"default": -2, "min": -24, "max": -1, "step": 1}),
+ "refiner_ckpt_name": (["None"] + folder_paths.get_filename_list("checkpoints"),),
+ "refiner_clip_skip": ("INT", {"default": -2, "min": -24, "max": -1, "step": 1}),
+ "positive_ascore": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01}),
+ "negative_ascore": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 1000.0, "step": 0.01}),
+ "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
+ "positive": ("STRING", {"default": "CLIP_POSITIVE", "multiline": True}),
+ "negative": ("STRING", {"default": "CLIP_NEGATIVE", "multiline": True}),
+ "token_normalization": (["none", "mean", "length", "length+mean"],),
+ "weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"],),
+ "empty_latent_width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
+ "empty_latent_height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
+ "batch_size": ("INT", {"default": 1, "min": 1, "max": 64})},
+ "optional": {"lora_stack": ("LORA_STACK", ), "cnet_stack": ("CONTROL_NET_STACK",),},
+ "hidden": { "prompt": "PROMPT", "my_unique_id": "UNIQUE_ID",},
+ }
+
+ RETURN_TYPES = ("SDXL_TUPLE", "LATENT", "VAE", "DEPENDENCIES",)
+ RETURN_NAMES = ("SDXL_TUPLE", "LATENT", "VAE", "DEPENDENCIES", )
+ FUNCTION = "efficientloaderSDXL"
+ CATEGORY = "Efficiency Nodes/Loaders"
+
+ def efficientloaderSDXL(self, base_ckpt_name, base_clip_skip, refiner_ckpt_name, refiner_clip_skip, positive_ascore,
+ negative_ascore, vae_name, positive, negative, token_normalization, weight_interpretation,
+ empty_latent_width, empty_latent_height, batch_size, lora_stack=None, cnet_stack=None,
+ prompt=None, my_unique_id=None):
+ clip_skip = (base_clip_skip, refiner_clip_skip)
+ lora_name = "None"
+ lora_model_strength = lora_clip_strength = 0
+ return super().efficientloader(base_ckpt_name, vae_name, clip_skip, lora_name, lora_model_strength, lora_clip_strength,
+ positive, negative, token_normalization, weight_interpretation, empty_latent_width, empty_latent_height,
+ batch_size, lora_stack=lora_stack, cnet_stack=cnet_stack, refiner_name=refiner_ckpt_name,
+ ascore=(positive_ascore, negative_ascore), prompt=prompt, my_unique_id=my_unique_id, loader_type="sdxl")
+
+#=======================================================================================================================
+# TSC Unpack SDXL Tuple
+class TSC_Unpack_SDXL_Tuple:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {"sdxl_tuple": ("SDXL_TUPLE",)},}
+
+ RETURN_TYPES = ("MODEL", "CLIP", "CONDITIONING","CONDITIONING", "MODEL", "CLIP", "CONDITIONING", "CONDITIONING",)
+ RETURN_NAMES = ("BASE_MODEL", "BASE_CLIP", "BASE_CONDITIONING+", "BASE_CONDITIONING-",
+ "REFINER_MODEL", "REFINER_CLIP","REFINER_CONDITIONING+","REFINER_CONDITIONING-",)
+ FUNCTION = "unpack_sdxl_tuple"
+ CATEGORY = "Efficiency Nodes/Misc"
+
+ def unpack_sdxl_tuple(self, sdxl_tuple):
+ return (sdxl_tuple[0], sdxl_tuple[1],sdxl_tuple[2],sdxl_tuple[3],
+ sdxl_tuple[4],sdxl_tuple[5],sdxl_tuple[6],sdxl_tuple[7],)
+
+# =======================================================================================================================
+# TSC Pack SDXL Tuple
+class TSC_Pack_SDXL_Tuple:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {"base_model": ("MODEL",),
+ "base_clip": ("CLIP",),
+ "base_positive": ("CONDITIONING",),
+ "base_negative": ("CONDITIONING",),
+ "refiner_model": ("MODEL",),
+ "refiner_clip": ("CLIP",),
+ "refiner_positive": ("CONDITIONING",),
+ "refiner_negative": ("CONDITIONING",),},}
+
+ RETURN_TYPES = ("SDXL_TUPLE",)
+ RETURN_NAMES = ("SDXL_TUPLE",)
+ FUNCTION = "pack_sdxl_tuple"
+ CATEGORY = "Efficiency Nodes/Misc"
+
+ def pack_sdxl_tuple(self, base_model, base_clip, base_positive, base_negative,
+ refiner_model, refiner_clip, refiner_positive, refiner_negative):
+ return ((base_model, base_clip, base_positive, base_negative,
+ refiner_model, refiner_clip, refiner_positive, refiner_negative),)
+
+########################################################################################################################
+# TSC LoRA Stacker
+class TSC_LoRA_Stacker:
+ modes = ["simple", "advanced"]
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ loras = ["None"] + folder_paths.get_filename_list("loras")
+ inputs = {
+ "required": {
+ "input_mode": (cls.modes,),
+ "lora_count": ("INT", {"default": 3, "min": 0, "max": 50, "step": 1}),
+ }
+ }
+
+ for i in range(1, 50):
+ inputs["required"][f"lora_name_{i}"] = (loras,)
+ inputs["required"][f"lora_wt_{i}"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
+ inputs["required"][f"model_str_{i}"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
+ inputs["required"][f"clip_str_{i}"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
+
+ inputs["optional"] = {
+ "lora_stack": ("LORA_STACK",)
+ }
+ return inputs
+
+ RETURN_TYPES = ("LORA_STACK",)
+ RETURN_NAMES = ("LORA_STACK",)
+ FUNCTION = "lora_stacker"
+ CATEGORY = "Efficiency Nodes/Stackers"
+
+ def lora_stacker(self, input_mode, lora_count, lora_stack=None, **kwargs):
+
+ # Extract values from kwargs
+ loras = [kwargs.get(f"lora_name_{i}") for i in range(1, lora_count + 1)]
+
+ # Create a list of tuples using provided parameters, exclude tuples with lora_name as "None"
+ if input_mode == "simple":
+ weights = [kwargs.get(f"lora_wt_{i}") for i in range(1, lora_count + 1)]
+ loras = [(lora_name, lora_weight, lora_weight) for lora_name, lora_weight in zip(loras, weights) if
+ lora_name != "None"]
+ else:
+ model_strs = [kwargs.get(f"model_str_{i}") for i in range(1, lora_count + 1)]
+ clip_strs = [kwargs.get(f"clip_str_{i}") for i in range(1, lora_count + 1)]
+ loras = [(lora_name, model_str, clip_str) for lora_name, model_str, clip_str in
+ zip(loras, model_strs, clip_strs) if lora_name != "None"]
+
+ # If lora_stack is not None, extend the loras list with lora_stack
+ if lora_stack is not None:
+ loras.extend([l for l in lora_stack if l[0] != "None"])
+
+ return (loras,)
+
+#=======================================================================================================================
+# TSC Control Net Stacker
+class TSC_Control_Net_Stacker:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {"control_net": ("CONTROL_NET",),
+ "image": ("IMAGE",),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
+ "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
+ "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})},
+ "optional": {"cnet_stack": ("CONTROL_NET_STACK",)},
+ }
+
+ RETURN_TYPES = ("CONTROL_NET_STACK",)
+ RETURN_NAMES = ("CNET_STACK",)
+ FUNCTION = "control_net_stacker"
+ CATEGORY = "Efficiency Nodes/Stackers"
+
+ def control_net_stacker(self, control_net, image, strength, start_percent, end_percent, cnet_stack=None):
+ # If control_net_stack is None, initialize as an empty list
+ cnet_stack = [] if cnet_stack is None else cnet_stack
+
+ # Extend the control_net_stack with the new tuple
+ cnet_stack.extend([(control_net, image, strength, start_percent, end_percent)])
+
+ return (cnet_stack,)
+
+#=======================================================================================================================
+# TSC Apply ControlNet Stack
+class TSC_Apply_ControlNet_Stack:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {"positive": ("CONDITIONING",),
+ "negative": ("CONDITIONING",)},
+ "optional": {"cnet_stack": ("CONTROL_NET_STACK",)}
+ }
+
+ RETURN_TYPES = ("CONDITIONING","CONDITIONING",)
+ RETURN_NAMES = ("CONDITIONING+","CONDITIONING-",)
+ FUNCTION = "apply_cnet_stack"
+ CATEGORY = "Efficiency Nodes/Stackers"
+
+ def apply_cnet_stack(self, positive, negative, cnet_stack=None):
+ if cnet_stack is None:
+ return (positive, negative)
+
+ for control_net_tuple in cnet_stack:
+ control_net, image, strength, start_percent, end_percent = control_net_tuple
+ controlnet_conditioning = ControlNetApplyAdvanced().apply_controlnet(positive, negative, control_net, image,
+ strength, start_percent, end_percent)
+ positive, negative = controlnet_conditioning[0], controlnet_conditioning[1]
+
+ return (positive, negative, )
+
+
+########################################################################################################################
+# TSC KSampler (Efficient)
+class TSC_KSampler:
+
+ empty_image = pil2tensor(Image.new('RGBA', (1, 1), (0, 0, 0, 0)))
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required":
+ {"model": ("MODEL",),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
+ "positive": ("CONDITIONING",),
+ "negative": ("CONDITIONING",),
+ "latent_image": ("LATENT",),
+ "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
+ "preview_method": (["auto", "latent2rgb", "taesd", "vae_decoded_only", "none"],),
+ "vae_decode": (["true", "true (tiled)", "false"],),
+ },
+ "optional": { "optional_vae": ("VAE",),
+ "script": ("SCRIPT",),},
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",},
+ }
+
+ RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "IMAGE", )
+ RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", "IMAGE", )
+ OUTPUT_NODE = True
+ FUNCTION = "sample"
+ CATEGORY = "Efficiency Nodes/Sampling"
+
+ def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
+ preview_method, vae_decode, denoise=1.0, prompt=None, extra_pnginfo=None, my_unique_id=None,
+ optional_vae=(None,), script=None, add_noise=None, start_at_step=None, end_at_step=None,
+ return_with_leftover_noise=None, sampler_type="regular"):
+
+ # Rename the vae variable
+ vae = optional_vae
+
+ # If vae is not connected, disable vae decoding
+ if vae == (None,) and vae_decode != "false":
+ print(f"{warning('KSampler(Efficient) Warning:')} No vae input detected, proceeding as if vae_decode was false.\n")
+ vae_decode = "false"
+
+ #---------------------------------------------------------------------------------------------------------------
+ # Unpack SDXL Tuple embedded in the 'model' channel
+ if sampler_type == "sdxl":
+ sdxl_tuple = model
+ model, _, positive, negative, refiner_model, _, refiner_positive, refiner_negative = sdxl_tuple
+ else:
+ refiner_model = refiner_positive = refiner_negative = None
+
+ #---------------------------------------------------------------------------------------------------------------
+ def keys_exist_in_script(*keys):
+ return any(key in script for key in keys) if script else False
+
+ #---------------------------------------------------------------------------------------------------------------
+ def vae_decode_latent(vae, samples, vae_decode):
+ return VAEDecodeTiled().decode(vae,samples,320)[0] if "tiled" in vae_decode else VAEDecode().decode(vae,samples)[0]
+
+ def vae_encode_image(vae, pixels, vae_decode):
+ return VAEEncodeTiled().encode(vae,pixels,320)[0] if "tiled" in vae_decode else VAEEncode().encode(vae,pixels)[0]
+
+ # ---------------------------------------------------------------------------------------------------------------
+ def process_latent_image(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
+ denoise, sampler_type, add_noise, start_at_step, end_at_step, return_with_leftover_noise,
+ refiner_model, refiner_positive, refiner_negative, vae, vae_decode, preview_method):
+
+ # Store originals
+ previous_preview_method = global_preview_method()
+ original_prepare_noise = comfy.sample.prepare_noise
+ original_KSampler = comfy.samplers.KSampler
+ original_model_str = str(model)
+
+ # Initialize output variables
+ samples = images = gifs = preview = cnet_imgs = None
+
+ try:
+ # Change the global preview method (temporarily)
+ set_preview_method(preview_method)
+
+ # ------------------------------------------------------------------------------------------------------
+ # Check if "noise" exists in the script before main sampling has taken place
+ if keys_exist_in_script("noise"):
+ rng_source, cfg_denoiser, add_seed_noise, m_seed, m_weight = script["noise"]
+ smZ_rng_source.rng_rand_source(rng_source) # this function monkey patches comfy.sample.prepare_noise
+ if cfg_denoiser:
+ comfy.samplers.KSampler = smZ_cfg_denoiser.SDKSampler
+ if add_seed_noise:
+ comfy.sample.prepare_noise = cg_mixed_seed_noise.get_mixed_noise_function(comfy.sample.prepare_noise, m_seed, m_weight)
+ else:
+ m_seed = m_weight = None
+ else:
+ rng_source = cfg_denoiser = add_seed_noise = m_seed = m_weight = None
+
+ # ------------------------------------------------------------------------------------------------------
+ # Check if "anim" exists in the script before main sampling has taken place
+ if keys_exist_in_script("anim"):
+ if preview_method != "none":
+ set_preview_method("none") # disable preview method
+ print(f"{warning('KSampler(Efficient) Warning:')} Live preview disabled for animatediff generations.")
+ motion_model, beta_schedule, context_options, frame_rate, loop_count, format, pingpong, save_image = script["anim"]
+ model = AnimateDiffLoaderWithContext().load_mm_and_inject_params(model, motion_model, beta_schedule, context_options)[0]
+
+ # ------------------------------------------------------------------------------------------------------
+ # Store run parameters as strings. Load previous stored samples if all parameters match.
+ latent_image_hash = tensor_to_hash(latent_image["samples"])
+ positive_hash = tensor_to_hash(positive[0][0])
+ negative_hash = tensor_to_hash(negative[0][0])
+ refiner_positive_hash = tensor_to_hash(refiner_positive[0][0]) if refiner_positive is not None else None
+ refiner_negative_hash = tensor_to_hash(refiner_negative[0][0]) if refiner_negative is not None else None
+
+ # Include motion_model, beta_schedule, and context_options as unique identifiers if they exist.
+ model_identifier = [original_model_str, motion_model, beta_schedule, context_options] if keys_exist_in_script("anim")\
+ else [original_model_str]
+
+ parameters = [model_identifier] + [seed, steps, cfg, sampler_name, scheduler, positive_hash, negative_hash,
+ latent_image_hash, denoise, sampler_type, add_noise, start_at_step,
+ end_at_step, return_with_leftover_noise, refiner_model, refiner_positive_hash,
+ refiner_negative_hash, rng_source, cfg_denoiser, add_seed_noise, m_seed, m_weight]
+
+ # Convert all elements in parameters to strings, except for the hash variable checks
+ parameters = [str(item) if not isinstance(item, type(latent_image_hash)) else item for item in parameters]
+
+ # Load previous latent if all parameters match, else returns 'None'
+ samples = load_ksampler_results("latent", my_unique_id, parameters)
+
+ if samples is None: # clear stored images
+ store_ksampler_results("image", my_unique_id, None)
+ store_ksampler_results("cnet_img", my_unique_id, None)
+
+ if samples is not None: # do not re-sample
+ images = load_ksampler_results("image", my_unique_id)
+ cnet_imgs = True # "True" will denote that it can be loaded provided the preprocessor matches
+
+ # Sample the latent_image(s) using the Comfy KSampler nodes
+ elif sampler_type == "regular":
+ samples = KSampler().sample(model, seed, steps, cfg, sampler_name, scheduler, positive, negative,
+ latent_image, denoise=denoise)[0] if denoise>0 else latent_image
+
+ elif sampler_type == "advanced":
+ samples = KSamplerAdvanced().sample(model, add_noise, seed, steps, cfg, sampler_name, scheduler,
+ positive, negative, latent_image, start_at_step, end_at_step,
+ return_with_leftover_noise, denoise=1.0)[0]
+
+ elif sampler_type == "sdxl":
+ # Disable refiner if refine_at_step is -1
+ if end_at_step == -1:
+ end_at_step = steps
+
+ # Perform base model sampling
+ add_noise = return_with_leftover_noise = True
+ samples = KSamplerAdvanced().sample(model, add_noise, seed, steps, cfg, sampler_name, scheduler,
+ positive, negative, latent_image, start_at_step, end_at_step,
+ return_with_leftover_noise, denoise=1.0)[0]
+
+ # Perform refiner model sampling
+ if refiner_model and end_at_step < steps:
+ add_noise = return_with_leftover_noise = False
+ samples = KSamplerAdvanced().sample(refiner_model, add_noise, seed, steps, cfg + REFINER_CFG_OFFSET,
+ sampler_name, scheduler, refiner_positive, refiner_negative,
+ samples, end_at_step, steps,
+ return_with_leftover_noise, denoise=1.0)[0]
+
+ # Cache the first pass samples in the 'last_helds' dictionary "latent" if not xyplot
+ if not any(keys_exist_in_script(key) for key in ["xyplot"]):
+ store_ksampler_results("latent", my_unique_id, samples, parameters)
+
+ # ------------------------------------------------------------------------------------------------------
+ # Check if "hiresfix" exists in the script after main sampling has taken place
+ if keys_exist_in_script("hiresfix"):
+ # Unpack the tuple from the script's "hiresfix" key
+ upscale_type, latent_upscaler, upscale_by, use_same_seed, hires_seed, hires_steps, hires_denoise,\
+ iterations, hires_control_net, hires_cnet_strength, preprocessor, preprocessor_imgs, \
+ latent_upscale_function, latent_upscale_model, pixel_upscale_model = script["hiresfix"]
+
+ # Define hires_seed
+ hires_seed = seed if use_same_seed else hires_seed
+
+ # Define latent_upscale_model
+ if latent_upscale_model is None:
+ latent_upscale_model = model
+ elif keys_exist_in_script("anim"):
+ latent_upscale_model = \
+ AnimateDiffLoaderWithContext().load_mm_and_inject_params(latent_upscale_model, motion_model,
+ beta_schedule, context_options)[0]
+
+ # Generate Preprocessor images and Apply Control Net
+ if hires_control_net is not None:
+ # Attempt to load previous "cnet_imgs" if previous images were loaded and preprocessor is same
+ if cnet_imgs is True:
+ cnet_imgs = load_ksampler_results("cnet_img", my_unique_id, [preprocessor])
+ # If cnet_imgs is None, generate new ones
+ if cnet_imgs is None:
+ if images is None:
+ images = vae_decode_latent(vae, samples, vae_decode)
+ store_ksampler_results("image", my_unique_id, images)
+ cnet_imgs = AIO_Preprocessor().execute(preprocessor, images)[0]
+ store_ksampler_results("cnet_img", my_unique_id, cnet_imgs, [preprocessor])
+ positive = ControlNetApply().apply_controlnet(positive, hires_control_net, cnet_imgs, hires_cnet_strength)[0]
+
+ # Iterate for the given number of iterations
+ if upscale_type == "latent":
+ for _ in range(iterations):
+ upscaled_latent_image = latent_upscale_function().upscale(samples, latent_upscaler, upscale_by)[0]
+ samples = KSampler().sample(latent_upscale_model, hires_seed, hires_steps, cfg, sampler_name, scheduler,
+ positive, negative, upscaled_latent_image, denoise=hires_denoise)[0]
+ images = None # set to None when samples is updated
+ elif upscale_type == "pixel":
+ if images is None:
+ images = vae_decode_latent(vae, samples, vae_decode)
+ store_ksampler_results("image", my_unique_id, images)
+ images = ImageUpscaleWithModel().upscale(pixel_upscale_model, images)[0]
+ images = ImageScaleBy().upscale(images, "nearest-exact", upscale_by/4)[0]
+
+ # ------------------------------------------------------------------------------------------------------
+ # Check if "tile" exists in the script after main sampling has taken place
+ if keys_exist_in_script("tile"):
+ # Unpack the tuple from the script's "tile" key
+ upscale_by, tile_size, tiling_strategy, tiling_steps, tile_seed, tiled_denoise,\
+ tile_controlnet, strength = script["tile"]
+
+ # Decode image, store if first decode
+ if images is None:
+ images = vae_decode_latent(vae, samples, vae_decode)
+ if not any(keys_exist_in_script(key) for key in ["xyplot", "hiresfix"]):
+ store_ksampler_results("image", my_unique_id, images)
+
+ # Upscale image
+ upscaled_image = ImageScaleBy().upscale(images, "nearest-exact", upscale_by)[0]
+ upscaled_latent = vae_encode_image(vae, upscaled_image, vae_decode)
+
+ # If using Control Net, Apply Control Net using upscaled_image and loaded control_net
+ if tile_controlnet is not None:
+ positive = ControlNetApply().apply_controlnet(positive, tile_controlnet, upscaled_image, 1)[0]
+
+ # Sample latent
+ TSampler = bnk_tiled_samplers.TiledKSampler
+ samples = TSampler().sample(model, tile_seed, tile_size, tile_size, tiling_strategy, tiling_steps, cfg,
+ sampler_name, scheduler, positive, negative, upscaled_latent,
+ denoise=tiled_denoise)[0]
+ images = None # set to None when samples is updated
+
+ # ------------------------------------------------------------------------------------------------------
+ # Check if "anim" exists in the script after the main sampling has taken place
+ if keys_exist_in_script("anim"):
+ if images is None:
+ images = vae_decode_latent(vae, samples, vae_decode)
+ if not any(keys_exist_in_script(key) for key in ["xyplot", "hiresfix", "tile"]):
+ store_ksampler_results("image", my_unique_id, images)
+ gifs = AnimateDiffCombine().generate_gif(images, frame_rate, loop_count, format=format,
+ pingpong=pingpong, save_image=save_image, prompt=prompt, extra_pnginfo=extra_pnginfo)["ui"]["gifs"]
+
+ # ------------------------------------------------------------------------------------------------------
+
+ # Decode image if not yet decoded
+ if "true" in vae_decode:
+ if images is None:
+ images = vae_decode_latent(vae, samples, vae_decode)
+ # Store decoded image as base image of no script is detected
+ if all(not keys_exist_in_script(key) for key in ["xyplot", "hiresfix", "tile", "anim"]):
+ store_ksampler_results("image", my_unique_id, images)
+
+ # Append Control Net Images (if exist)
+ if cnet_imgs is not None and not True:
+ if preprocessor_imgs and upscale_type == "latent":
+ if keys_exist_in_script("xyplot"):
+ print(
+ f"{warning('HighRes-Fix Warning:')} Preprocessor images auto-disabled when XY Plotting.")
+ else:
+ # Resize cnet_imgs if necessary and stack
+ if images.shape[1:3] != cnet_imgs.shape[1:3]: # comparing height and width
+ cnet_imgs = quick_resize(cnet_imgs, images.shape)
+ images = torch.cat([images, cnet_imgs], dim=0)
+
+ # Define preview images
+ if keys_exist_in_script("anim"):
+ preview = {"gifs": gifs, "images": list()}
+ elif preview_method == "none" or (preview_method == "vae_decoded_only" and vae_decode == "false"):
+ preview = {"images": list()}
+ elif images is not None:
+ preview = PreviewImage().save_images(images, prompt=prompt, extra_pnginfo=extra_pnginfo)["ui"]
+
+ # Define a dummy output image
+ if images is None and vae_decode == "false":
+ images = TSC_KSampler.empty_image
+
+ finally:
+ # Restore global changes
+ set_preview_method(previous_preview_method)
+ comfy.samplers.KSampler = original_KSampler
+ comfy.sample.prepare_noise = original_prepare_noise
+
+ return samples, images, gifs, preview
+
+ # ---------------------------------------------------------------------------------------------------------------
+ # Clean globally stored objects of non-existant nodes
+ globals_cleanup(prompt)
+
+ # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ # If not XY Plotting
+ if not keys_exist_in_script("xyplot"):
+
+ # Process latent image
+ samples, images, gifs, preview = process_latent_image(model, seed, steps, cfg, sampler_name, scheduler,
+ positive, negative, latent_image, denoise, sampler_type, add_noise,
+ start_at_step, end_at_step, return_with_leftover_noise, refiner_model,
+ refiner_positive, refiner_negative, vae, vae_decode, preview_method)
+
+ if sampler_type == "sdxl":
+ result = (sdxl_tuple, samples, vae, images,)
+ else:
+ result = (model, positive, negative, samples, vae, images,)
+
+ if preview is None:
+ return {"result": result}
+ else:
+ return {"ui": preview, "result": result}
+
+ # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ # If XY Plot
+ elif keys_exist_in_script("xyplot"):
+
+ # If no vae connected, throw errors
+ if vae == (None,):
+ print(f"{error('KSampler(Efficient) Error:')} VAE input must be connected in order to use the XY Plot script.")
+
+ return {"ui": {"images": list()},
+ "result": (model, positive, negative, latent_image, vae, TSC_KSampler.empty_image,)}
+
+ # If vae_decode is not set to true, print message that changing it to true
+ if "true" not in vae_decode:
+ print(f"{warning('KSampler(Efficient) Warning:')} VAE decoding must be set to \'true\'"
+ " for the XY Plot script, proceeding as if \'true\'.\n")
+
+ #___________________________________________________________________________________________________________
+ # Initialize, unpack, and clean variables for the XY Plot script
+ vae_name = None
+ ckpt_name = None
+ clip = None
+ clip_skip = None
+ refiner_name = None
+ refiner_clip = None
+ refiner_clip_skip = None
+ positive_prompt = None
+ negative_prompt = None
+ ascore = None
+ empty_latent_width = None
+ empty_latent_height = None
+ lora_stack = None
+ cnet_stack = None
+
+ # Split the 'samples' tensor
+ samples_tensors = torch.split(latent_image['samples'], 1, dim=0)
+
+ # Check if 'noise_mask' exists and split if it does
+ if 'noise_mask' in latent_image:
+ noise_mask_tensors = torch.split(latent_image['noise_mask'], 1, dim=0)
+ latent_tensors = [{'samples': img, 'noise_mask': mask} for img, mask in
+ zip(samples_tensors, noise_mask_tensors)]
+ else:
+ latent_tensors = [{'samples': img} for img in samples_tensors]
+
+ # Set latent only to the first of the batch
+ latent_image = latent_tensors[0]
+
+ # Unpack script Tuple (X_type, X_value, Y_type, Y_value, grid_spacing, Y_label_orientation, dependencies)
+ X_type, X_value, Y_type, Y_value, grid_spacing, Y_label_orientation, cache_models, xyplot_as_output_image,\
+ xyplot_id, dependencies = script["xyplot"]
+
+ #_______________________________________________________________________________________________________
+ # The below section is used to check wether the XY_type is allowed for the Ksampler instance being used.
+ # If not the correct type, this section will abort the xy plot script.
+
+ samplers = {
+ "regular": {
+ "disallowed": ["AddNoise", "ReturnNoise", "StartStep", "EndStep", "RefineStep",
+ "Refiner", "Refiner On/Off", "AScore+", "AScore-"],
+ "name": "KSampler (Efficient)"
+ },
+ "advanced": {
+ "disallowed": ["RefineStep", "Denoise", "RefineStep", "Refiner", "Refiner On/Off",
+ "AScore+", "AScore-"],
+ "name": "KSampler Adv. (Efficient)"
+ },
+ "sdxl": {
+ "disallowed": ["AddNoise", "EndStep", "Denoise"],
+ "name": "KSampler SDXL (Eff.)"
+ }
+ }
+
+ # Define disallowed XY_types for each ksampler type
+ def get_ksampler_details(sampler_type):
+ return samplers.get(sampler_type, {"disallowed": [], "name": ""})
+
+ def suggest_ksampler(X_type, Y_type, current_sampler):
+ for sampler, details in samplers.items():
+ if sampler != current_sampler and X_type not in details["disallowed"] and Y_type not in details["disallowed"]:
+ return details["name"]
+ return "a different KSampler"
+
+ # In your main function or code segment:
+ details = get_ksampler_details(sampler_type)
+ disallowed_XY_types = details["disallowed"]
+ ksampler_name = details["name"]
+
+ if X_type in disallowed_XY_types or Y_type in disallowed_XY_types:
+ error_prefix = f"{error(f'{ksampler_name} Error:')}"
+
+ failed_type = []
+ if X_type in disallowed_XY_types:
+ failed_type.append(f"X_type: '{X_type}'")
+ if Y_type in disallowed_XY_types:
+ failed_type.append(f"Y_type: '{Y_type}'")
+
+ suggested_ksampler = suggest_ksampler(X_type, Y_type, sampler_type)
+
+ print(f"{error_prefix} Invalid value for {' and '.join(failed_type)}. "
+ f"Use {suggested_ksampler} for this XY Plot type."
+ f"\nDisallowed XY_types for this KSampler are: {', '.join(disallowed_XY_types)}.")
+
+ return {"ui": {"images": list()},
+ "result": (model, positive, negative, latent_image, vae, TSC_KSampler.empty_image,)}
+
+ #_______________________________________________________________________________________________________
+ # Unpack Effficient Loader dependencies
+ if dependencies is not None:
+ vae_name, ckpt_name, clip, clip_skip, refiner_name, refiner_clip, refiner_clip_skip,\
+ positive_prompt, negative_prompt, token_normalization, weight_interpretation, ascore,\
+ empty_latent_width, empty_latent_height, lora_stack, cnet_stack = dependencies
+
+ #_______________________________________________________________________________________________________
+ # Printout XY Plot values to be processed
+ def process_xy_for_print(value, replacement, type_):
+
+ if type_ == "Seeds++ Batch" and isinstance(value, list):
+ return [v + seed for v in value] # Add seed to every entry in the list
+
+ elif type_ == "Scheduler" and isinstance(value, tuple):
+ return value[0] # Return only the first entry of the tuple
+
+ elif type_ == "VAE" and isinstance(value, list):
+ # For each string in the list, extract the filename from the path
+ return [os.path.basename(v) for v in value]
+
+ elif (type_ == "Checkpoint" or type_ == "Refiner") and isinstance(value, list):
+ # For each tuple in the list, return only the first value if the second or third value is None
+ return [(os.path.basename(v[0]),) + v[1:] if v[1] is None or v[2] is None
+ else (os.path.basename(v[0]), v[1]) if v[2] is None
+ else (os.path.basename(v[0]),) + v[1:] for v in value]
+
+ elif type_ == "LoRA" and isinstance(value, list):
+ # Return only the first Tuple of each inner array
+ return [[(os.path.basename(v[0][0]),) + v[0][1:], "..."] if len(v) > 1
+ else [(os.path.basename(v[0][0]),) + v[0][1:]] for v in value]
+
+ elif type_ == "LoRA Batch" and isinstance(value, list):
+ # Extract the basename of the first value of the first tuple from each sublist
+ return [os.path.basename(v[0][0]) for v in value if v and isinstance(v[0], tuple) and v[0][0]]
+
+ elif (type_ == "LoRA Wt" or type_ == "LoRA MStr") and isinstance(value, list):
+ # Extract the first value of the first tuple from each sublist
+ return [v[0][1] for v in value if v and isinstance(v[0], tuple)]
+
+ elif type_ == "LoRA CStr" and isinstance(value, list):
+ # Extract the first value of the first tuple from each sublist
+ return [v[0][2] for v in value if v and isinstance(v[0], tuple)]
+
+ elif type_ == "ControlNetStrength" and isinstance(value, list):
+ # Extract the third entry of the first tuple from each inner list
+ return [round(inner_list[0][2], 3) for inner_list in value]
+
+ elif type_ == "ControlNetStart%" and isinstance(value, list):
+ # Extract the third entry of the first tuple from each inner list
+ return [round(inner_list[0][3], 3) for inner_list in value]
+
+ elif type_ == "ControlNetEnd%" and isinstance(value, list):
+ # Extract the third entry of the first tuple from each inner list
+ return [round(inner_list[0][4], 3) for inner_list in value]
+
+ elif isinstance(value, tuple):
+ return tuple(replacement if v is None else v for v in value)
+
+ else:
+ return replacement if value is None else value
+
+ # Determine the replacements based on X_type and Y_type
+ replacement_X = scheduler if X_type == 'Sampler' else clip_skip if X_type == 'Checkpoint' else None
+ replacement_Y = scheduler if Y_type == 'Sampler' else clip_skip if Y_type == 'Checkpoint' else None
+
+ # Process X_value and Y_value
+ X_value_processed = process_xy_for_print(X_value, replacement_X, X_type)
+ Y_value_processed = process_xy_for_print(Y_value, replacement_Y, Y_type)
+
+ print(info("-" * 40))
+ print(info('XY Plot Script Inputs:'))
+ print(info(f"(X) {X_type}:"))
+ for item in X_value_processed:
+ print(info(f" {item}"))
+ print(info(f"(Y) {Y_type}:"))
+ for item in Y_value_processed:
+ print(info(f" {item}"))
+ print(info("-" * 40))
+
+ #_______________________________________________________________________________________________________
+ # Perform various initializations in this section
+
+ # If not caching models, set to 1.
+ if cache_models == "False":
+ vae_cache = ckpt_cache = lora_cache = refn_cache = 1
+ else:
+ # Retrieve cache numbers
+ vae_cache, ckpt_cache, lora_cache, refn_cache = get_cache_numbers("XY Plot")
+ # Pack cache numbers in a tuple
+ cache = (vae_cache, ckpt_cache, lora_cache, refn_cache)
+
+ # Add seed to every entry in the list
+ X_value = [v + seed for v in X_value] if "Seeds++ Batch" == X_type else X_value
+ Y_value = [v + seed for v in Y_value] if "Seeds++ Batch" == Y_type else Y_value
+
+ # Embedd original prompts into prompt variables
+ positive_prompt = (positive_prompt, positive_prompt)
+ negative_prompt = (negative_prompt, negative_prompt)
+
+ # Set lora_stack to None if one of types are LoRA
+ if "LoRA" in X_type or "LoRA" in Y_type:
+ lora_stack = None
+
+ # Define the manipulated and static Control Net Variables with a tuple with shape (cn_1, cn_2, cn_3).
+ # The information in this tuple will be used by the plotter to properly plot Control Net XY input types.
+ cn_1, cn_2, cn_3 = None, None, None
+ # If X_type has "ControlNet" or both X_type and Y_type have "ControlNet"
+ if "ControlNet" in X_type:
+ cn_1, cn_2, cn_3 = X_value[0][0][2], X_value[0][0][3], X_value[0][0][4]
+ # If only Y_type has "ControlNet" and not X_type
+ elif "ControlNet" in Y_type:
+ cn_1, cn_2, cn_3 = Y_value[0][0][2], Y_value[0][0][3], Y_value[0][0][4]
+ # Additional checks for other substrings
+ if "ControlNetStrength" in X_type or "ControlNetStrength" in Y_type:
+ cn_1 = None
+ if "ControlNetStart%" in X_type or "ControlNetStart%" in Y_type:
+ cn_2 = None
+ if "ControlNetEnd%" in X_type or "ControlNetEnd%" in Y_type:
+ cn_3 = None
+ # Embed the information in cnet_stack
+ cnet_stack = (cnet_stack, (cn_1, cn_2, cn_3))
+
+ # Optimize image generation by prioritization:
+ priority = [
+ "Checkpoint",
+ "Refiner",
+ "LoRA",
+ "VAE",
+ ]
+ conditioners = {
+ "Positive Prompt S/R",
+ "Negative Prompt S/R",
+ "AScore+",
+ "AScore-",
+ "Clip Skip",
+ "Clip Skip (Refiner)",
+ "ControlNetStrength",
+ "ControlNetStart%",
+ "ControlNetEnd%"
+ }
+ # Get priority values; return a high number if the type is not in priority list
+ x_priority = priority.index(X_type) if X_type in priority else 999
+ y_priority = priority.index(Y_type) if Y_type in priority else 999
+
+ # Check if both are conditioners
+ are_both_conditioners = X_type in conditioners and Y_type in conditioners
+
+ # Special cases
+ is_special_case = (
+ (X_type == "Refiner On/Off" and Y_type in ["RefineStep", "Steps"]) or
+ (X_type == "Nothing" and Y_type != "Nothing")
+ )
+
+ # Determine whether to flip
+ flip_xy = (y_priority < x_priority and not are_both_conditioners) or is_special_case
+
+ # Perform the flip if necessary
+ if flip_xy:
+ X_type, Y_type = Y_type, X_type
+ X_value, Y_value = Y_value, X_value
+
+ #_______________________________________________________________________________________________________
+ # The below code will clean from the cache any ckpt/vae/lora models it will not be reusing.
+ # Note: Special LoRA types will not trigger cache: "LoRA Batch", "LoRA Wt", "LoRA MStr", "LoRA CStr"
+
+ # Map the type names to the dictionaries
+ dict_map = {"VAE": [], "Checkpoint": [], "LoRA": [], "Refiner": []}
+
+ # Create a list of tuples with types and values
+ type_value_pairs = [(X_type, X_value.copy()), (Y_type, Y_value.copy())]
+
+ # Iterate over type-value pairs
+ for t, v in type_value_pairs:
+ if t in dict_map:
+ # Flatten the list of lists of tuples if the type is "LoRA"
+ if t == "LoRA":
+ dict_map[t] = [item for sublist in v for item in sublist]
+ else:
+ dict_map[t] = v
+
+ vae_dict = dict_map.get("VAE", [])
+
+ # Construct ckpt_dict and also update vae_dict based on the third entry of the tuples in dict_map["Checkpoint"]
+ if dict_map.get("Checkpoint", []):
+ ckpt_dict = [t[0] for t in dict_map["Checkpoint"]]
+ for t in dict_map["Checkpoint"]:
+ if t[2] is not None and t[2] != "Baked VAE":
+ vae_dict.append(t[2])
+ else:
+ ckpt_dict = []
+
+ lora_dict = [[t,] for t in dict_map.get("LoRA", [])] if dict_map.get("LoRA", []) else []
+
+ # Construct refn_dict
+ if dict_map.get("Refiner", []):
+ refn_dict = [t[0] for t in dict_map["Refiner"]]
+ else:
+ refn_dict = []
+
+ # If both ckpt_dict and lora_dict are not empty, manipulate lora_dict as described
+ if ckpt_dict and lora_dict:
+ lora_dict = [(lora_stack, ckpt) for ckpt in ckpt_dict for lora_stack in lora_dict]
+ # If lora_dict is not empty and ckpt_dict is empty, insert ckpt_name into each tuple in lora_dict
+ elif lora_dict:
+ lora_dict = [(lora_stack, ckpt_name) for lora_stack in lora_dict]
+
+ # Avoid caching models accross both X and Y
+ if X_type == "Checkpoint":
+ lora_dict = []
+ refn_dict = []
+ elif X_type == "Refiner":
+ ckpt_dict = []
+ lora_dict = []
+ elif X_type == "LoRA":
+ ckpt_dict = []
+ refn_dict = []
+
+ ### Print dict_arrays for debugging
+ ###print(f"vae_dict={vae_dict}\nckpt_dict={ckpt_dict}\nlora_dict={lora_dict}\nrefn_dict={refn_dict}")
+
+ # Clean values that won't be reused
+ clear_cache_by_exception(xyplot_id, vae_dict=vae_dict, ckpt_dict=ckpt_dict, lora_dict=lora_dict, refn_dict=refn_dict)
+
+ ### Print loaded_objects for debugging
+ ###print_loaded_objects_entries()
+
+ #_______________________________________________________________________________________________________
+ # Function that changes appropiate variables for next processed generations (also generates XY_labels)
+ def define_variable(var_type, var, add_noise, seed, steps, start_at_step, end_at_step,
+ return_with_leftover_noise, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name,
+ clip_skip, refiner_name, refiner_clip_skip, positive_prompt, negative_prompt, ascore,
+ lora_stack, cnet_stack, var_label, num_label):
+
+ # Define default max label size limit
+ max_label_len = 42
+
+ # If var_type is "AddNoise", update 'add_noise' with 'var', and generate text label
+ if var_type == "AddNoise":
+ add_noise = var
+ text = f"AddNoise: {add_noise}"
+
+ # If var_type is "Seeds++ Batch", generate text label
+ elif var_type == "Seeds++ Batch":
+ seed = var
+ text = f"Seed: {seed}"
+
+ # If var_type is "Steps", update 'steps' with 'var' and generate text label
+ elif var_type == "Steps":
+ steps = var
+ text = f"Steps: {steps}"
+
+ # If var_type is "StartStep", update 'start_at_step' with 'var' and generate text label
+ elif var_type == "StartStep":
+ start_at_step = var
+ text = f"StartStep: {start_at_step}"
+
+ # If var_type is "EndStep", update 'end_at_step' with 'var' and generate text label
+ elif var_type == "EndStep":
+ end_at_step = var
+ text = f"EndStep: {end_at_step}"
+
+ # If var_type is "RefineStep", update 'end_at_step' with 'var' and generate text label
+ elif var_type == "RefineStep":
+ end_at_step = var
+ text = f"RefineStep: {end_at_step}"
+
+ # If var_type is "ReturnNoise", update 'return_with_leftover_noise' with 'var', and generate text label
+ elif var_type == "ReturnNoise":
+ return_with_leftover_noise = var
+ text = f"ReturnNoise: {return_with_leftover_noise}"
+
+ # If var_type is "CFG Scale", update cfg with var and generate text label
+ elif var_type == "CFG Scale":
+ cfg = var
+ text = f"CFG: {round(cfg,2)}"
+
+ # If var_type is "Sampler", update sampler_name and scheduler with var, and generate text label
+ elif var_type == "Sampler":
+ sampler_name = var[0]
+ if var[1] == "":
+ text = f"{sampler_name}"
+ else:
+ if var[1] != None:
+ scheduler = (var[1], scheduler[1])
+ else:
+ scheduler = (scheduler[1], scheduler[1])
+ text = f"{sampler_name} ({scheduler[0]})"
+ text = text.replace("ancestral", "a").replace("uniform", "u").replace("exponential","exp")
+
+ # If var_type is "Scheduler", update scheduler and generate labels
+ elif var_type == "Scheduler":
+ if len(var) == 2:
+ scheduler = (var[0], scheduler[1])
+ text = f"{sampler_name} ({scheduler[0]})"
+ else:
+ scheduler = (var, scheduler[1])
+ text = f"{scheduler[0]}"
+ text = text.replace("ancestral", "a").replace("uniform", "u").replace("exponential","exp")
+
+ # If var_type is "Denoise", update denoise and generate labels
+ elif var_type == "Denoise":
+ denoise = var
+ text = f"Denoise: {round(denoise, 2)}"
+
+ # If var_type is "VAE", update vae_name and generate labels
+ elif var_type == "VAE":
+ vae_name = var
+ vae_filename = os.path.splitext(os.path.basename(vae_name))[0]
+ text = f"VAE: {vae_filename}"
+
+ # If var_type is "Positive Prompt S/R", update positive_prompt and generate labels
+ elif var_type == "Positive Prompt S/R":
+ search_txt, replace_txt = var
+ if replace_txt != None:
+ positive_prompt = (positive_prompt[1].replace(search_txt, replace_txt, 1), positive_prompt[1])
+ else:
+ positive_prompt = (positive_prompt[1], positive_prompt[1])
+ replace_txt = search_txt
+ text = f"{replace_txt}"
+
+ # If var_type is "Negative Prompt S/R", update negative_prompt and generate labels
+ elif var_type == "Negative Prompt S/R":
+ search_txt, replace_txt = var
+ if replace_txt:
+ negative_prompt = (negative_prompt[1].replace(search_txt, replace_txt, 1), negative_prompt[1])
+ else:
+ negative_prompt = (negative_prompt[1], negative_prompt[1])
+ replace_txt = search_txt
+ text = f"(-) {replace_txt}"
+
+ # If var_type is "AScore+", update positive ascore and generate labels
+ elif var_type == "AScore+":
+ ascore = (var,ascore[1])
+ text = f"+AScore: {ascore[0]}"
+
+ # If var_type is "AScore-", update negative ascore and generate labels
+ elif var_type == "AScore-":
+ ascore = (ascore[0],var)
+ text = f"-AScore: {ascore[1]}"
+
+ # If var_type is "Checkpoint", update model and clip (if needed) and generate labels
+ elif var_type == "Checkpoint":
+ ckpt_name = var[0]
+ if var[1] == None:
+ clip_skip = (clip_skip[1],clip_skip[1])
+ else:
+ clip_skip = (var[1],clip_skip[1])
+ if var[2] != None:
+ vae_name = var[2]
+ ckpt_filename = os.path.splitext(os.path.basename(ckpt_name))[0]
+ text = f"{ckpt_filename}"
+
+ # If var_type is "Refiner", update model and clip (if needed) and generate labels
+ elif var_type == "Refiner":
+ refiner_name = var[0]
+ if var[1] == None:
+ refiner_clip_skip = (refiner_clip_skip[1],refiner_clip_skip[1])
+ else:
+ refiner_clip_skip = (var[1],refiner_clip_skip[1])
+ ckpt_filename = os.path.splitext(os.path.basename(refiner_name))[0]
+ text = f"{ckpt_filename}"
+
+ # If var_type is "Refiner On/Off", set end_at_step = max steps and generate labels
+ elif var_type == "Refiner On/Off":
+ end_at_step = int(var * steps)
+ text = f"Refiner: {'On' if var < 1 else 'Off'}"
+
+ elif var_type == "Clip Skip":
+ clip_skip = (var, clip_skip[1])
+ text = f"ClipSkip ({clip_skip[0]})"
+
+ elif var_type == "Clip Skip (Refiner)":
+ refiner_clip_skip = (var, refiner_clip_skip[1])
+ text = f"RefClipSkip ({refiner_clip_skip[0]})"
+
+ elif "LoRA" in var_type:
+ if not lora_stack:
+ lora_stack = var.copy()
+ else:
+ # Updating the first tuple of lora_stack
+ lora_stack[0] = tuple(v if v is not None else lora_stack[0][i] for i, v in enumerate(var[0]))
+
+ max_label_len = 50 + (12 * (len(lora_stack) - 1))
+ lora_name, lora_model_wt, lora_clip_wt = lora_stack[0]
+ lora_filename = os.path.splitext(os.path.basename(lora_name))[0]
+
+ if var_type == "LoRA":
+ if len(lora_stack) == 1:
+ lora_model_wt = format(float(lora_model_wt), ".2f").rstrip('0').rstrip('.')
+ lora_clip_wt = format(float(lora_clip_wt), ".2f").rstrip('0').rstrip('.')
+ lora_filename = lora_filename[:max_label_len - len(f"LoRA: ({lora_model_wt})")]
+ if lora_model_wt == lora_clip_wt:
+ text = f"LoRA: {lora_filename}({lora_model_wt})"
+ else:
+ text = f"LoRA: {lora_filename}({lora_model_wt},{lora_clip_wt})"
+ elif len(lora_stack) > 1:
+ lora_filenames = [os.path.splitext(os.path.basename(lora_name))[0] for lora_name, _, _ in
+ lora_stack]
+ lora_details = [(format(float(lora_model_wt), ".2f").rstrip('0').rstrip('.'),
+ format(float(lora_clip_wt), ".2f").rstrip('0').rstrip('.')) for
+ _, lora_model_wt, lora_clip_wt in lora_stack]
+ non_name_length = sum(
+ len(f"({lora_details[i][0]},{lora_details[i][1]})") + 2 for i in range(len(lora_stack)))
+ available_space = max_label_len - non_name_length
+ max_name_length = available_space // len(lora_stack)
+ lora_filenames = [filename[:max_name_length] for filename in lora_filenames]
+ text_elements = [
+ f"{lora_filename}({lora_details[i][0]})" if lora_details[i][0] == lora_details[i][1]
+ else f"{lora_filename}({lora_details[i][0]},{lora_details[i][1]})" for i, lora_filename in
+ enumerate(lora_filenames)]
+ text = " ".join(text_elements)
+
+ elif var_type == "LoRA Batch":
+ text = f"LoRA: {lora_filename}"
+
+ elif var_type == "LoRA Wt":
+ lora_model_wt = format(float(lora_model_wt), ".2f").rstrip('0').rstrip('.')
+ text = f"LoRA Wt: {lora_model_wt}"
+
+ elif var_type == "LoRA MStr":
+ lora_model_wt = format(float(lora_model_wt), ".2f").rstrip('0').rstrip('.')
+ text = f"LoRA Mstr: {lora_model_wt}"
+
+ elif var_type == "LoRA CStr":
+ lora_clip_wt = format(float(lora_clip_wt), ".2f").rstrip('0').rstrip('.')
+ text = f"LoRA Cstr: {lora_clip_wt}"
+
+ elif var_type in ["ControlNetStrength", "ControlNetStart%", "ControlNetEnd%"]:
+ if "Strength" in var_type:
+ entry_index = 2
+ elif "Start%" in var_type:
+ entry_index = 3
+ elif "End%" in var_type:
+ entry_index = 4
+
+ # If the first entry of cnet_stack is None, set it to var
+ if cnet_stack[0] is None:
+ cnet_stack = (var, cnet_stack[1])
+ else:
+ # Extract the desired entry from var's first tuple
+ entry_from_var = var[0][entry_index]
+
+ # Extract the first tuple from cnet_stack[0][0] and make it mutable
+ first_cn_entry = list(cnet_stack[0][0])
+
+ # Replace the appropriate entry
+ first_cn_entry[entry_index] = entry_from_var
+
+ # Further update first_cn_entry based on cnet_stack[1]
+ for i, value in enumerate(cnet_stack[1][-3:]): # Considering last 3 entries
+ if value is not None:
+ first_cn_entry[i + 2] = value # "+2" to offset for the first 2 entries of the tuple
+
+ # Convert back to tuple for the updated values
+ updated_first_entry = tuple(first_cn_entry)
+
+ # Construct the updated cnet_stack[0] using the updated_first_entry and the rest of the values from cnet_stack[0]
+ updated_cnet_stack_0 = [updated_first_entry] + list(cnet_stack[0][1:])
+
+ # Update cnet_stack
+ cnet_stack = (updated_cnet_stack_0, cnet_stack[1])
+
+ # Print the desired value
+ text = f'{var_type}: {round(cnet_stack[0][0][entry_index], 3)}'
+
+ elif var_type == "XY_Capsule":
+ text = var.getLabel()
+
+ else: # No matching type found
+ text=""
+
+ def truncate_texts(texts, num_label, max_label_len):
+ truncate_length = max(min(max(len(text) for text in texts), max_label_len), 24)
+
+ return [text if len(text) <= truncate_length else text[:truncate_length] + "..." for text in
+ texts]
+
+ # Add the generated text to var_label if it's not full
+ if len(var_label) < num_label:
+ var_label.append(text)
+
+ # If var_type VAE , truncate entries in the var_label list when it's full
+ if len(var_label) == num_label and (var_type == "VAE" or var_type == "Checkpoint"
+ or var_type == "Refiner" or "LoRA" in var_type):
+ var_label = truncate_texts(var_label, num_label, max_label_len)
+
+ # Return the modified variables
+ return add_noise, seed, steps, start_at_step, end_at_step, return_with_leftover_noise, cfg,\
+ sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip, \
+ refiner_name, refiner_clip_skip, positive_prompt, negative_prompt, ascore,\
+ lora_stack, cnet_stack, var_label
+
+ #_______________________________________________________________________________________________________
+ # The function below is used to optimally load Checkpoint/LoRA/VAE models between generations.
+ def define_model(model, clip, clip_skip, refiner_model, refiner_clip, refiner_clip_skip,
+ ckpt_name, refiner_name, positive, negative, refiner_positive, refiner_negative,
+ positive_prompt, negative_prompt, ascore, vae, vae_name, lora_stack, cnet_stack, index,
+ types, xyplot_id, cache, sampler_type, empty_latent_width, empty_latent_height):
+
+ # Variable to track wether to encode prompt or not
+ encode = False
+ encode_refiner = False
+
+ # Unpack types tuple
+ X_type, Y_type = types
+
+ # Note: Index is held at 0 when Y_type == "Nothing"
+
+ # Load Checkpoint if required. If Y_type is LoRA, required models will be loaded by load_lora func.
+ if (X_type == "Checkpoint" and index == 0 and Y_type != "LoRA"):
+ if lora_stack is None:
+ model, clip, _ = load_checkpoint(ckpt_name, xyplot_id, cache=cache[1])
+ else: # Load Efficient Loader LoRA
+ model, clip = load_lora(lora_stack, ckpt_name, xyplot_id,
+ cache=None, ckpt_cache=cache[1])
+ encode = True
+
+ # Load LoRA if required
+ elif (X_type == "LoRA" and index == 0):
+ # Don't cache Checkpoints
+ model, clip = load_lora(lora_stack, ckpt_name, xyplot_id, cache=cache[2])
+ encode = True
+ elif Y_type == "LoRA": # X_type must be Checkpoint, so cache those as defined
+ model, clip = load_lora(lora_stack, ckpt_name, xyplot_id,
+ cache=None, ckpt_cache=cache[1])
+ encode = True
+ elif X_type == "LoRA Batch" or X_type == "LoRA Wt" or X_type == "LoRA MStr" or X_type == "LoRA CStr":
+ # Don't cache Checkpoints or LoRAs
+ model, clip = load_lora(lora_stack, ckpt_name, xyplot_id, cache=0)
+ encode = True
+
+ if (X_type == "Refiner" and index == 0) or Y_type == "Refiner":
+ refiner_model, refiner_clip, _ = \
+ load_checkpoint(refiner_name, xyplot_id, output_vae=False, cache=cache[3], ckpt_type="refn")
+ encode_refiner = True
+
+ # Encode base prompt if required
+ encode_types = ["Positive Prompt S/R", "Negative Prompt S/R", "Clip Skip", "ControlNetStrength",
+ "ControlNetStart%", "ControlNetEnd%"]
+ if (X_type in encode_types and index == 0) or Y_type in encode_types:
+ encode = True
+
+ # Encode refiner prompt if required
+ encode_refiner_types = ["Positive Prompt S/R", "Negative Prompt S/R", "AScore+", "AScore-",
+ "Clip Skip (Refiner)"]
+ if (X_type in encode_refiner_types and index == 0) or Y_type in encode_refiner_types:
+ encode_refiner = True
+
+ # Encode base prompt
+ if encode == True:
+ positive, negative, clip = \
+ encode_prompts(positive_prompt, negative_prompt, token_normalization, weight_interpretation,
+ clip, clip_skip, refiner_clip, refiner_clip_skip, ascore, sampler_type == "sdxl",
+ empty_latent_width, empty_latent_height, return_type="base")
+ # Apply ControlNet Stack if given
+ if cnet_stack:
+ controlnet_conditioning = TSC_Apply_ControlNet_Stack().apply_cnet_stack(positive, negative, cnet_stack)
+ positive, negative = controlnet_conditioning[0], controlnet_conditioning[1]
+
+ if encode_refiner == True:
+ refiner_positive, refiner_negative, refiner_clip = \
+ encode_prompts(positive_prompt, negative_prompt, token_normalization, weight_interpretation,
+ clip, clip_skip, refiner_clip, refiner_clip_skip, ascore, sampler_type == "sdxl",
+ empty_latent_width, empty_latent_height, return_type="refiner")
+
+ # Load VAE if required
+ if (X_type == "VAE" and index == 0) or Y_type == "VAE":
+ #vae = load_vae(vae_name, xyplot_id, cache=cache[0])
+ vae = get_bvae_by_ckpt_name(ckpt_name) if vae_name == "Baked VAE" \
+ else load_vae(vae_name, xyplot_id, cache=cache[0])
+ elif X_type == "Checkpoint" and index == 0 and vae_name:
+ vae = get_bvae_by_ckpt_name(ckpt_name) if vae_name == "Baked VAE" \
+ else load_vae(vae_name, xyplot_id, cache=cache[0])
+
+ return model, positive, negative, refiner_model, refiner_positive, refiner_negative, vae
+
+ # ______________________________________________________________________________________________________
+ # The below function is used to generate the results based on all the processed variables
+ def process_values(model, refiner_model, add_noise, seed, steps, start_at_step, end_at_step,
+ return_with_leftover_noise, cfg, sampler_name, scheduler, positive, negative,
+ refiner_positive, refiner_negative, latent_image, denoise, vae, vae_decode,
+ sampler_type, latent_list=[], image_tensor_list=[], image_pil_list=[], xy_capsule=None):
+
+ capsule_result = None
+ if xy_capsule is not None:
+ capsule_result = xy_capsule.get_result(model, clip, vae)
+ if capsule_result is not None:
+ image, latent = capsule_result
+ latent_list.append(latent)
+
+ if capsule_result is None:
+
+ samples, images, _, _ = process_latent_image(model, seed, steps, cfg, sampler_name, scheduler, positive, negative,
+ latent_image, denoise, sampler_type, add_noise, start_at_step,
+ end_at_step, return_with_leftover_noise, refiner_model,
+ refiner_positive, refiner_negative, vae, vae_decode, preview_method)
+
+ # Add the latent tensor to the tensors list
+ latent_list.append(samples)
+
+ # Decode the latent tensor if required
+ image = images if images is not None else vae_decode_latent(vae, samples, vae_decode)
+
+ if xy_capsule is not None:
+ xy_capsule.set_result(image, samples)
+
+ # Add the resulting image tensor to image_tensor_list
+ image_tensor_list.append(image)
+
+ # Convert the image from tensor to PIL Image and add it to the image_pil_list
+ image_pil_list.append(tensor2pil(image))
+
+ # Return the touched variables
+ return latent_list, image_tensor_list, image_pil_list
+
+ # ______________________________________________________________________________________________________
+ # The below section is the heart of the XY Plot image generation
+
+ # Initiate Plot label text variables X/Y_label
+ X_label = []
+ Y_label = []
+
+ # Store the KSamplers original scheduler inside the same scheduler variable
+ scheduler = (scheduler, scheduler)
+
+ # Store the Eff Loaders original clip_skips inside the same clip_skip variables
+ clip_skip = (clip_skip, clip_skip)
+ refiner_clip_skip = (refiner_clip_skip, refiner_clip_skip)
+
+ # Store types in a Tuple for easy function passing
+ types = (X_type, Y_type)
+
+ # Clone original model parameters
+ def clone_or_none(*originals):
+ cloned_items = []
+ for original in originals:
+ try:
+ cloned_items.append(original.clone())
+ except (AttributeError, TypeError):
+ # If not clonable, just append the original item
+ cloned_items.append(original)
+ return cloned_items
+ original_model, original_clip, original_positive, original_negative,\
+ original_refiner_model, original_refiner_clip, original_refiner_positive, original_refiner_negative =\
+ clone_or_none(model, clip, positive, negative, refiner_model, refiner_clip, refiner_positive, refiner_negative)
+
+ # Fill Plot Rows (X)
+ for X_index, X in enumerate(X_value):
+
+ # Define X parameters and generate labels
+ add_noise, seed, steps, start_at_step, end_at_step, return_with_leftover_noise, cfg,\
+ sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip,\
+ refiner_name, refiner_clip_skip, positive_prompt, negative_prompt, ascore,\
+ lora_stack, cnet_stack, X_label = \
+ define_variable(X_type, X, add_noise, seed, steps, start_at_step, end_at_step,
+ return_with_leftover_noise, cfg, sampler_name, scheduler, denoise, vae_name,
+ ckpt_name, clip_skip, refiner_name, refiner_clip_skip, positive_prompt,
+ negative_prompt, ascore, lora_stack, cnet_stack, X_label, len(X_value))
+
+ if X_type != "Nothing" and Y_type == "Nothing":
+ if X_type == "XY_Capsule":
+ model, clip, refiner_model, refiner_clip = \
+ clone_or_none(original_model, original_clip, original_refiner_model, original_refiner_clip)
+ model, clip, vae = X.pre_define_model(model, clip, vae)
+
+ # Models & Conditionings
+ model, positive, negative, refiner_model, refiner_positive, refiner_negative, vae = \
+ define_model(model, clip, clip_skip[0], refiner_model, refiner_clip, refiner_clip_skip[0],
+ ckpt_name, refiner_name, positive, negative, refiner_positive, refiner_negative,
+ positive_prompt[0], negative_prompt[0], ascore, vae, vae_name, lora_stack, cnet_stack[0],
+ 0, types, xyplot_id, cache, sampler_type, empty_latent_width, empty_latent_height)
+
+ xy_capsule = None
+ if X_type == "XY_Capsule":
+ xy_capsule = X
+
+ # Generate Results
+ latent_list, image_tensor_list, image_pil_list = \
+ process_values(model, refiner_model, add_noise, seed, steps, start_at_step, end_at_step,
+ return_with_leftover_noise, cfg, sampler_name, scheduler[0], positive, negative,
+ refiner_positive, refiner_negative, latent_image, denoise, vae, vae_decode, sampler_type, xy_capsule=xy_capsule)
+
+ elif X_type != "Nothing" and Y_type != "Nothing":
+ for Y_index, Y in enumerate(Y_value):
+
+ if Y_type == "XY_Capsule" or X_type == "XY_Capsule":
+ model, clip, refiner_model, refiner_clip = \
+ clone_or_none(original_model, original_clip, original_refiner_model, original_refiner_clip)
+
+ if Y_type == "XY_Capsule" and X_type == "XY_Capsule":
+ Y.set_x_capsule(X)
+
+ # Define Y parameters and generate labels
+ add_noise, seed, steps, start_at_step, end_at_step, return_with_leftover_noise, cfg,\
+ sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip,\
+ refiner_name, refiner_clip_skip, positive_prompt, negative_prompt, ascore,\
+ lora_stack, cnet_stack, Y_label = \
+ define_variable(Y_type, Y, add_noise, seed, steps, start_at_step, end_at_step,
+ return_with_leftover_noise, cfg, sampler_name, scheduler, denoise, vae_name,
+ ckpt_name, clip_skip, refiner_name, refiner_clip_skip, positive_prompt,
+ negative_prompt, ascore, lora_stack, cnet_stack, Y_label, len(Y_value))
+
+ if Y_type == "XY_Capsule":
+ model, clip, vae = Y.pre_define_model(model, clip, vae)
+ elif X_type == "XY_Capsule":
+ model, clip, vae = X.pre_define_model(model, clip, vae)
+
+ # Models & Conditionings
+ model, positive, negative, refiner_model, refiner_positive, refiner_negative, vae = \
+ define_model(model, clip, clip_skip[0], refiner_model, refiner_clip, refiner_clip_skip[0],
+ ckpt_name, refiner_name, positive, negative, refiner_positive, refiner_negative,
+ positive_prompt[0], negative_prompt[0], ascore, vae, vae_name, lora_stack, cnet_stack[0],
+ Y_index, types, xyplot_id, cache, sampler_type, empty_latent_width,
+ empty_latent_height)
+
+ # Generate Results
+ xy_capsule = None
+ if Y_type == "XY_Capsule":
+ xy_capsule = Y
+
+ latent_list, image_tensor_list, image_pil_list = \
+ process_values(model, refiner_model, add_noise, seed, steps, start_at_step, end_at_step,
+ return_with_leftover_noise, cfg, sampler_name, scheduler[0],
+ positive, negative, refiner_positive, refiner_negative, latent_image,
+ denoise, vae, vae_decode, sampler_type, xy_capsule=xy_capsule)
+
+ # Clean up cache
+ if cache_models == "False":
+ clear_cache_by_exception(xyplot_id, vae_dict=[], ckpt_dict=[], lora_dict=[], refn_dict=[])
+ else:
+ # Avoid caching models accross both X and Y
+ if X_type == "Checkpoint":
+ clear_cache_by_exception(xyplot_id, lora_dict=[], refn_dict=[])
+ elif X_type == "Refiner":
+ clear_cache_by_exception(xyplot_id, ckpt_dict=[], lora_dict=[])
+ elif X_type == "LoRA":
+ clear_cache_by_exception(xyplot_id, ckpt_dict=[], refn_dict=[])
+
+ # __________________________________________________________________________________________________________
+ # Function for printing all plot variables (WARNING: This function is an absolute mess)
+ def print_plot_variables(X_type, Y_type, X_value, Y_value, add_noise, seed, steps, start_at_step, end_at_step,
+ return_with_leftover_noise, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name,
+ clip_skip, refiner_name, refiner_clip_skip, ascore, lora_stack, cnet_stack, sampler_type,
+ num_rows, num_cols, i_height, i_width):
+
+ print("-" * 40) # Print an empty line followed by a separator line
+ print(f"{xyplot_message('XY Plot Results:')}")
+
+ def get_vae_name(X_type, Y_type, X_value, Y_value, vae_name):
+ if X_type == "VAE":
+ vae_name = "\n ".join(map(lambda x: os.path.splitext(os.path.basename(str(x)))[0], X_value))
+ elif Y_type == "VAE":
+ vae_name = "\n ".join(map(lambda y: os.path.splitext(os.path.basename(str(y)))[0], Y_value))
+ elif vae_name:
+ vae_name = os.path.splitext(os.path.basename(str(vae_name)))[0]
+ else:
+ vae_name = ""
+ return vae_name
+
+ def get_clip_skip(X_type, Y_type, X_value, Y_value, cskip, mode):
+ clip_type = "Clip Skip" if mode == "ckpt" else "Clip Skip (Refiner)"
+ if X_type == clip_type:
+ cskip = ", ".join(map(str, X_value))
+ elif Y_type == clip_type:
+ cskip = ", ".join(map(str, Y_value))
+ elif cskip[1] != None:
+ cskip = cskip[1]
+ else:
+ cskip = ""
+ return cskip
+
+ def get_checkpoint_name(X_type, Y_type, X_value, Y_value, ckpt_name, clip_skip, mode, vae_name=None):
+
+ # If ckpt_name is None, return it as is
+ if ckpt_name is not None:
+ ckpt_name = os.path.basename(ckpt_name)
+
+ # Define types based on mode
+ primary_type = "Checkpoint" if mode == "ckpt" else "Refiner"
+ clip_type = "Clip Skip" if mode == "ckpt" else "Clip Skip (Refiner)"
+
+ # Determine ckpt and othr based on primary type
+ if X_type == primary_type:
+ ckpt_type, ckpt_value = X_type, X_value.copy()
+ othr_type, othr_value = Y_type, Y_value.copy()
+ elif Y_type == primary_type:
+ ckpt_type, ckpt_value = Y_type, Y_value.copy()
+ othr_type, othr_value = X_type, X_value.copy()
+ else:
+ # Process as per original function if mode is "ckpt"
+ clip_skip = get_clip_skip(X_type, Y_type, X_value, Y_value, clip_skip, mode)
+ if mode == "ckpt":
+ if vae_name:
+ vae_name = get_vae_name(X_type, Y_type, X_value, Y_value, vae_name)
+ return ckpt_name, clip_skip, vae_name
+ else:
+ # For refn mode
+ return ckpt_name, clip_skip
+
+ # Process clip skip based on mode
+ if othr_type == clip_type:
+ clip_skip = ", ".join(map(str, othr_value))
+ elif ckpt_value[0][1] != None:
+ clip_skip = None
+
+ # Process vae_name based on mode
+ if mode == "ckpt":
+ if othr_type == "VAE":
+ vae_name = get_vae_name(X_type, Y_type, X_value, Y_value, vae_name)
+ elif ckpt_value[0][2] != None:
+ vae_name = None
+
+ def format_name(v, _type):
+ base = os.path.basename(v[0])
+ if _type == clip_type and v[1] is not None:
+ return base
+ elif _type == "VAE" and v[1] is not None and v[2] is not None:
+ return f"{base}({v[1]})"
+ elif v[1] is not None and v[2] is not None:
+ return f"{base}({v[1]}) + vae:{v[2]}"
+ elif v[1] is not None:
+ return f"{base}({v[1]})"
+ else:
+ return base
+
+ ckpt_name = "\n ".join([format_name(v, othr_type) for v in ckpt_value])
+ if mode == "ckpt":
+ return ckpt_name, clip_skip, vae_name
+ else:
+ return ckpt_name, clip_skip
+
+ def get_lora_name(X_type, Y_type, X_value, Y_value, lora_stack=None):
+ lora_name = lora_wt = lora_model_str = lora_clip_str = None
+
+ # Check for all possible LoRA types
+ lora_types = ["LoRA", "LoRA Batch", "LoRA Wt", "LoRA MStr", "LoRA CStr"]
+
+ if X_type not in lora_types and Y_type not in lora_types:
+ if lora_stack:
+ names_list = []
+ for name, model_wt, clip_wt in lora_stack:
+ base_name = os.path.splitext(os.path.basename(name))[0]
+ formatted_str = f"{base_name}({round(model_wt, 3)},{round(clip_wt, 3)})"
+ names_list.append(formatted_str)
+ lora_name = f"[{', '.join(names_list)}]"
+ else:
+ if X_type in lora_types:
+ value = get_lora_sublist_name(X_type, X_value)
+ if X_type == "LoRA":
+ lora_name = value
+ lora_model_str = None
+ lora_clip_str = None
+ if X_type == "LoRA Batch":
+ lora_name = value
+ lora_model_str = X_value[0][0][1] if lora_model_str is None else lora_model_str
+ lora_clip_str = X_value[0][0][2] if lora_clip_str is None else lora_clip_str
+ elif X_type == "LoRA MStr":
+ lora_name = os.path.basename(X_value[0][0][0]) if lora_name is None else lora_name
+ lora_model_str = value
+ lora_clip_str = X_value[0][0][2] if lora_clip_str is None else lora_clip_str
+ elif X_type == "LoRA CStr":
+ lora_name = os.path.basename(X_value[0][0][0]) if lora_name is None else lora_name
+ lora_model_str = X_value[0][0][1] if lora_model_str is None else lora_model_str
+ lora_clip_str = value
+ elif X_type == "LoRA Wt":
+ lora_name = os.path.basename(X_value[0][0][0]) if lora_name is None else lora_name
+ lora_wt = value
+
+ if Y_type in lora_types:
+ value = get_lora_sublist_name(Y_type, Y_value)
+ if Y_type == "LoRA":
+ lora_name = value
+ lora_model_str = None
+ lora_clip_str = None
+ if Y_type == "LoRA Batch":
+ lora_name = value
+ lora_model_str = Y_value[0][0][1] if lora_model_str is None else lora_model_str
+ lora_clip_str = Y_value[0][0][2] if lora_clip_str is None else lora_clip_str
+ elif Y_type == "LoRA MStr":
+ lora_name = os.path.basename(Y_value[0][0][0]) if lora_name is None else lora_name
+ lora_model_str = value
+ lora_clip_str = Y_value[0][0][2] if lora_clip_str is None else lora_clip_str
+ elif Y_type == "LoRA CStr":
+ lora_name = os.path.basename(Y_value[0][0][0]) if lora_name is None else lora_name
+ lora_model_str = Y_value[0][0][1] if lora_model_str is None else lora_model_str
+ lora_clip_str = value
+ elif Y_type == "LoRA Wt":
+ lora_name = os.path.basename(Y_value[0][0][0]) if lora_name is None else lora_name
+ lora_wt = value
+
+ return lora_name, lora_wt, lora_model_str, lora_clip_str
+
+ def get_lora_sublist_name(lora_type, lora_value):
+ if lora_type == "LoRA" or lora_type == "LoRA Batch":
+ formatted_sublists = []
+ for sublist in lora_value:
+ formatted_entries = []
+ for x in sublist:
+ base_name = os.path.splitext(os.path.basename(str(x[0])))[0]
+ formatted_str = f"{base_name}({round(x[1], 3)},{round(x[2], 3)})" if lora_type == "LoRA" else f"{base_name}"
+ formatted_entries.append(formatted_str)
+ formatted_sublists.append(f"{', '.join(formatted_entries)}")
+ return "\n ".join(formatted_sublists)
+ elif lora_type == "LoRA MStr":
+ return ", ".join([str(round(x[0][1], 3)) for x in lora_value])
+ elif lora_type == "LoRA CStr":
+ return ", ".join([str(round(x[0][2], 3)) for x in lora_value])
+ elif lora_type == "LoRA Wt":
+ return ", ".join([str(round(x[0][1], 3)) for x in lora_value]) # assuming LoRA Wt uses the second value
+ else:
+ return ""
+
+ # VAE, Checkpoint, Clip Skip, LoRA
+ ckpt_name, clip_skip, vae_name = get_checkpoint_name(X_type, Y_type, X_value, Y_value, ckpt_name, clip_skip, "ckpt", vae_name)
+ lora_name, lora_wt, lora_model_str, lora_clip_str = get_lora_name(X_type, Y_type, X_value, Y_value, lora_stack)
+ refiner_name, refiner_clip_skip = get_checkpoint_name(X_type, Y_type, X_value, Y_value, refiner_name, refiner_clip_skip, "refn")
+
+ # AddNoise
+ add_noise = ", ".join(map(str, X_value)) if X_type == "AddNoise" else ", ".join(
+ map(str, Y_value)) if Y_type == "AddNoise" else add_noise
+
+ # Seeds++ Batch
+ seed = "\n ".join(map(str, X_value)) if X_type == "Seeds++ Batch" else "\n ".join(
+ map(str, Y_value)) if Y_type == "Seeds++ Batch" else seed
+
+ # Steps
+ steps = ", ".join(map(str, X_value)) if X_type == "Steps" else ", ".join(
+ map(str, Y_value)) if Y_type == "Steps" else steps
+
+ # StartStep
+ start_at_step = ", ".join(map(str, X_value)) if X_type == "StartStep" else ", ".join(
+ map(str, Y_value)) if Y_type == "StartStep" else start_at_step
+
+ # EndStep/RefineStep
+ end_at_step = ", ".join(map(str, X_value)) if X_type in ["EndStep", "RefineStep"] else ", ".join(
+ map(str, Y_value)) if Y_type in ["EndStep", "RefineStep"] else end_at_step
+
+ # ReturnNoise
+ return_with_leftover_noise = ", ".join(map(str, X_value)) if X_type == "ReturnNoise" else ", ".join(
+ map(str, Y_value)) if Y_type == "ReturnNoise" else return_with_leftover_noise
+
+ # CFG
+ cfg = ", ".join(map(str, X_value)) if X_type == "CFG Scale" else ", ".join(
+ map(str, Y_value)) if Y_type == "CFG Scale" else round(cfg,3)
+
+ # Sampler/Scheduler
+ if X_type == "Sampler":
+ if Y_type == "Scheduler":
+ sampler_name = ", ".join([f"{x[0]}" for x in X_value])
+ scheduler = ", ".join([f"{y}" for y in Y_value])
+ else:
+ sampler_name = ", ".join([f"{x[0]}({x[1] if x[1] != '' and x[1] is not None else scheduler[1]})" for x in X_value])
+ scheduler = "_"
+ elif Y_type == "Sampler":
+ if X_type == "Scheduler":
+ sampler_name = ", ".join([f"{y[0]}" for y in Y_value])
+ scheduler = ", ".join([f"{x}" for x in X_value])
+ else:
+ sampler_name = ", ".join([f"{y[0]}({y[1] if y[1] != '' and y[1] is not None else scheduler[1]})" for y in Y_value])
+ scheduler = "_"
+ else:
+ scheduler = ", ".join([str(x[0]) if isinstance(x, tuple) else str(x) for x in X_value]) if X_type == "Scheduler" else \
+ ", ".join([str(y[0]) if isinstance(y, tuple) else str(y) for y in Y_value]) if Y_type == "Scheduler" else scheduler[0]
+
+ # Denoise
+ denoise = ", ".join(map(str, X_value)) if X_type == "Denoise" else ", ".join(
+ map(str, Y_value)) if Y_type == "Denoise" else round(denoise,3)
+
+ # Check if ascore is None
+ if ascore is None:
+ pos_ascore = neg_ascore = None
+ else:
+ # Ascore+
+ pos_ascore = (", ".join(map(str, X_value)) if X_type == "Ascore+"
+ else ", ".join(map(str, Y_value)) if Y_type == "Ascore+" else round(ascore[0],3))
+ # Ascore-
+ neg_ascore = (", ".join(map(str, X_value)) if X_type == "Ascore-"
+ else ", ".join(map(str, Y_value)) if Y_type == "Ascore-" else round(ascore[1],3))
+
+ #..........................................PRINTOUTS....................................................
+ print(f"(X) {X_type}")
+ print(f"(Y) {Y_type}")
+ print(f"img_count: {len(X_value)*len(Y_value)}")
+ print(f"img_dims: {i_height} x {i_width}")
+ print(f"plot_dim: {num_cols} x {num_rows}")
+ print(f"ckpt: {ckpt_name if ckpt_name is not None else ''}")
+ if clip_skip:
+ print(f"clip_skip: {clip_skip}")
+ if sampler_type == "sdxl":
+ if refiner_clip_skip == "_":
+ print(f"refiner(clipskip): {refiner_name if refiner_name is not None else ''}")
+ else:
+ print(f"refiner: {refiner_name if refiner_name is not None else ''}")
+ print(f"refiner_clip_skip: {refiner_clip_skip if refiner_clip_skip is not None else ''}")
+ print(f"+ascore: {pos_ascore if pos_ascore is not None else ''}")
+ print(f"-ascore: {neg_ascore if neg_ascore is not None else ''}")
+ if lora_name:
+ print(f"lora: {lora_name}")
+ if lora_wt:
+ print(f"lora_wt: {lora_wt}")
+ if lora_model_str:
+ print(f"lora_mstr: {lora_model_str}")
+ if lora_clip_str:
+ print(f"lora_cstr: {lora_clip_str}")
+ if vae_name:
+ print(f"vae: {vae_name}")
+ if sampler_type == "advanced":
+ print(f"add_noise: {add_noise}")
+ print(f"seed: {seed}")
+ print(f"steps: {steps}")
+ if sampler_type == "advanced":
+ print(f"start_at_step: {start_at_step}")
+ print(f"end_at_step: {end_at_step}")
+ print(f"return_noise: {return_with_leftover_noise}")
+ if sampler_type == "sdxl":
+ print(f"start_at_step: {start_at_step}")
+ if X_type == "Refiner On/Off":
+ print(f"refine_at_percent: {X_value[0]}")
+ elif Y_type == "Refiner On/Off":
+ print(f"refine_at_percent: {Y_value[0]}")
+ else:
+ print(f"refine_at_step: {end_at_step}")
+ print(f"cfg: {cfg}")
+ if scheduler == "_":
+ print(f"sampler(scheduler): {sampler_name}")
+ else:
+ print(f"sampler: {sampler_name}")
+ print(f"scheduler: {scheduler}")
+ if sampler_type == "regular":
+ print(f"denoise: {denoise}")
+
+ if X_type == "Positive Prompt S/R" or Y_type == "Positive Prompt S/R":
+ positive_prompt = ", ".join([str(x[0]) if i == 0 else str(x[1]) for i, x in enumerate(
+ X_value)]) if X_type == "Positive Prompt S/R" else ", ".join(
+ [str(y[0]) if i == 0 else str(y[1]) for i, y in
+ enumerate(Y_value)]) if Y_type == "Positive Prompt S/R" else positive_prompt
+ print(f"+prompt_s/r: {positive_prompt}")
+
+ if X_type == "Negative Prompt S/R" or Y_type == "Negative Prompt S/R":
+ negative_prompt = ", ".join([str(x[0]) if i == 0 else str(x[1]) for i, x in enumerate(
+ X_value)]) if X_type == "Negative Prompt S/R" else ", ".join(
+ [str(y[0]) if i == 0 else str(y[1]) for i, y in
+ enumerate(Y_value)]) if Y_type == "Negative Prompt S/R" else negative_prompt
+ print(f"-prompt_s/r: {negative_prompt}")
+
+ if "ControlNet" in X_type or "ControlNet" in Y_type:
+ cnet_strength, cnet_start_pct, cnet_end_pct = cnet_stack[1]
+
+ if "ControlNet" in X_type:
+ if "Strength" in X_type:
+ cnet_strength = [str(round(inner_list[0][2], 3)) for inner_list in X_value if
+ isinstance(inner_list, list) and
+ inner_list and isinstance(inner_list[0], tuple) and len(inner_list[0]) >= 3]
+ if "Start%" in X_type:
+ cnet_start_pct = [str(round(inner_list[0][3], 3)) for inner_list in X_value if
+ isinstance(inner_list, list) and
+ inner_list and isinstance(inner_list[0], tuple) and len(inner_list[0]) >= 3]
+ if "End%" in X_type:
+ cnet_end_pct = [str(round(inner_list[0][4], 3)) for inner_list in X_value if
+ isinstance(inner_list, list) and
+ inner_list and isinstance(inner_list[0], tuple) and len(inner_list[0]) >= 3]
+ if "ControlNet" in Y_type:
+ if "Strength" in Y_type:
+ cnet_strength = [str(round(inner_list[0][2], 3)) for inner_list in Y_value if
+ isinstance(inner_list, list) and
+ inner_list and isinstance(inner_list[0], tuple) and len(
+ inner_list[0]) >= 3]
+ if "Start%" in Y_type:
+ cnet_start_pct = [str(round(inner_list[0][3], 3)) for inner_list in Y_value if
+ isinstance(inner_list, list) and
+ inner_list and isinstance(inner_list[0], tuple) and len(
+ inner_list[0]) >= 3]
+ if "End%" in Y_type:
+ cnet_end_pct = [str(round(inner_list[0][4], 3)) for inner_list in Y_value if
+ isinstance(inner_list, list) and
+ inner_list and isinstance(inner_list[0], tuple) and len(
+ inner_list[0]) >= 3]
+
+ if "ControlNet" in X_type or "ControlNet" in Y_type:
+ print(f"cnet_strength: {', '.join(cnet_strength) if isinstance(cnet_strength, list) else cnet_strength}")
+ print(f"cnet_start%: {', '.join(cnet_start_pct) if isinstance(cnet_start_pct, list) else cnet_start_pct}")
+ print(f"cnet_end%: {', '.join(cnet_end_pct) if isinstance(cnet_end_pct, list) else cnet_end_pct}")
+
+ # ______________________________________________________________________________________________________
+ def adjusted_font_size(text, initial_font_size, i_width):
+ font = ImageFont.truetype(str(Path(font_path)), initial_font_size)
+ text_width = font.getlength(text)
+
+ if text_width > (i_width * 0.9):
+ scaling_factor = 0.9 # A value less than 1 to shrink the font size more aggressively
+ new_font_size = int(initial_font_size * (i_width / text_width) * scaling_factor)
+ else:
+ new_font_size = initial_font_size
+
+ return new_font_size
+
+ # ______________________________________________________________________________________________________
+
+ def rearrange_list_A(arr, num_cols, num_rows):
+ new_list = []
+ for i in range(num_rows):
+ for j in range(num_cols):
+ index = j * num_rows + i
+ new_list.append(arr[index])
+ return new_list
+
+ def rearrange_list_B(arr, num_rows, num_cols):
+ new_list = []
+ for i in range(num_rows):
+ for j in range(num_cols):
+ index = i * num_cols + j
+ new_list.append(arr[index])
+ return new_list
+
+ # Extract plot dimensions
+ num_rows = max(len(Y_value) if Y_value is not None else 0, 1)
+ num_cols = max(len(X_value) if X_value is not None else 0, 1)
+
+ # Flip X & Y results back if flipped earlier (for Checkpoint/LoRA For loop optimizations)
+ if flip_xy == True:
+ X_type, Y_type = Y_type, X_type
+ X_value, Y_value = Y_value, X_value
+ X_label, Y_label = Y_label, X_label
+ num_rows, num_cols = num_cols, num_rows
+ image_pil_list = rearrange_list_A(image_pil_list, num_rows, num_cols)
+ else:
+ image_pil_list = rearrange_list_B(image_pil_list, num_rows, num_cols)
+ image_tensor_list = rearrange_list_A(image_tensor_list, num_cols, num_rows)
+ latent_list = rearrange_list_A(latent_list, num_cols, num_rows)
+
+ # Extract final image dimensions
+ i_height, i_width = image_tensor_list[0].shape[1], image_tensor_list[0].shape[2]
+
+ # Print XY Plot Results
+ print_plot_variables(X_type, Y_type, X_value, Y_value, add_noise, seed, steps, start_at_step, end_at_step,
+ return_with_leftover_noise, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name,
+ clip_skip, refiner_name, refiner_clip_skip, ascore, lora_stack, cnet_stack,
+ sampler_type, num_rows, num_cols, i_height, i_width)
+
+ # Concatenate the 'samples' and 'noise_mask' tensors along the first dimension (dim=0)
+ keys = latent_list[0].keys()
+ result = {}
+ for key in keys:
+ tensors = [d[key] for d in latent_list]
+ result[key] = torch.cat(tensors, dim=0)
+ latent_list = result
+
+ # Store latent_list as last latent
+ ###update_value_by_id("latent", my_unique_id, latent_list)
+
+ # Calculate the dimensions of the white background image
+ border_size_top = i_width // 15
+
+ # Longest Y-label length
+ if len(Y_label) > 0:
+ Y_label_longest = max(len(s) for s in Y_label)
+ else:
+ # Handle the case when the sequence is empty
+ Y_label_longest = 0 # or any other appropriate value
+
+ Y_label_scale = min(Y_label_longest + 4,24) / 24
+
+ if Y_label_orientation == "Vertical":
+ border_size_left = border_size_top
+ else: # Assuming Y_label_orientation is "Horizontal"
+ # border_size_left is now min(i_width, i_height) plus 20% of the difference between the two
+ border_size_left = min(i_width, i_height) + int(0.2 * abs(i_width - i_height))
+ border_size_left = int(border_size_left * Y_label_scale)
+
+ # Modify the border size, background width and x_offset initialization based on Y_type and Y_label_orientation
+ if Y_type == "Nothing":
+ bg_width = num_cols * i_width + (num_cols - 1) * grid_spacing
+ x_offset_initial = 0
+ else:
+ if Y_label_orientation == "Vertical":
+ bg_width = num_cols * i_width + (num_cols - 1) * grid_spacing + 3 * border_size_left
+ x_offset_initial = border_size_left * 3
+ else: # Assuming Y_label_orientation is "Horizontal"
+ bg_width = num_cols * i_width + (num_cols - 1) * grid_spacing + border_size_left
+ x_offset_initial = border_size_left
+
+ # Modify the background height based on X_type
+ if X_type == "Nothing":
+ bg_height = num_rows * i_height + (num_rows - 1) * grid_spacing
+ y_offset = 0
+ else:
+ bg_height = num_rows * i_height + (num_rows - 1) * grid_spacing + 3 * border_size_top
+ y_offset = border_size_top * 3
+
+ # Create the white background image
+ background = Image.new('RGBA', (int(bg_width), int(bg_height)), color=(255, 255, 255, 255))
+
+ for row in range(num_rows):
+
+ # Initialize the X_offset
+ x_offset = x_offset_initial
+
+ for col in range(num_cols):
+ # Calculate the index for image_pil_list
+ index = col * num_rows + row
+ img = image_pil_list[index]
+
+ # Paste the image
+ background.paste(img, (x_offset, y_offset))
+
+ if row == 0 and X_type != "Nothing":
+ # Assign text
+ text = X_label[col]
+
+ # Add the corresponding X_value as a label above the image
+ initial_font_size = int(48 * img.width / 512)
+ font_size = adjusted_font_size(text, initial_font_size, img.width)
+ label_height = int(font_size*1.5)
+
+ # Create a white background label image
+ label_bg = Image.new('RGBA', (img.width, label_height), color=(255, 255, 255, 0))
+ d = ImageDraw.Draw(label_bg)
+
+ # Create the font object
+ font = ImageFont.truetype(str(Path(font_path)), font_size)
+
+ # Calculate the text size and the starting position
+ _, _, text_width, text_height = d.textbbox([0,0], text, font=font)
+ text_x = (img.width - text_width) // 2
+ text_y = (label_height - text_height) // 2
+
+ # Add the text to the label image
+ d.text((text_x, text_y), text, fill='black', font=font)
+
+ # Calculate the available space between the top of the background and the top of the image
+ available_space = y_offset - label_height
+
+ # Calculate the new Y position for the label image
+ label_y = available_space // 2
+
+ # Paste the label image above the image on the background using alpha_composite()
+ background.alpha_composite(label_bg, (x_offset, label_y))
+
+ if col == 0 and Y_type != "Nothing":
+ # Assign text
+ text = Y_label[row]
+
+ # Add the corresponding Y_value as a label to the left of the image
+ if Y_label_orientation == "Vertical":
+ initial_font_size = int(48 * i_width / 512) # Adjusting this to be same as X_label size
+ font_size = adjusted_font_size(text, initial_font_size, i_width)
+ else: # Assuming Y_label_orientation is "Horizontal"
+ initial_font_size = int(48 * (border_size_left/Y_label_scale) / 512) # Adjusting this to be same as X_label size
+ font_size = adjusted_font_size(text, initial_font_size, int(border_size_left/Y_label_scale))
+
+ # Create a white background label image
+ label_bg = Image.new('RGBA', (img.height, int(font_size*1.2)), color=(255, 255, 255, 0))
+ d = ImageDraw.Draw(label_bg)
+
+ # Create the font object
+ font = ImageFont.truetype(str(Path(font_path)), font_size)
+
+ # Calculate the text size and the starting position
+ _, _, text_width, text_height = d.textbbox([0,0], text, font=font)
+ text_x = (img.height - text_width) // 2
+ text_y = (font_size - text_height) // 2
+
+ # Add the text to the label image
+ d.text((text_x, text_y), text, fill='black', font=font)
+
+ # Rotate the label_bg 90 degrees counter-clockwise only if Y_label_orientation is "Vertical"
+ if Y_label_orientation == "Vertical":
+ label_bg = label_bg.rotate(90, expand=True)
+
+ # Calculate the available space between the left of the background and the left of the image
+ available_space = x_offset - label_bg.width
+
+ # Calculate the new X position for the label image
+ label_x = available_space // 2
+
+ # Calculate the Y position for the label image based on its orientation
+ if Y_label_orientation == "Vertical":
+ label_y = y_offset + (img.height - label_bg.height) // 2
+ else: # Assuming Y_label_orientation is "Horizontal"
+ label_y = y_offset + img.height - (img.height - label_bg.height) // 2
+
+ # Paste the label image to the left of the image on the background using alpha_composite()
+ background.alpha_composite(label_bg, (label_x, label_y))
+
+ # Update the x_offset
+ x_offset += img.width + grid_spacing
+
+ # Update the y_offset
+ y_offset += img.height + grid_spacing
+
+ xy_plot_image = pil2tensor(background)
+
+ # Generate the preview_images
+ preview_images = PreviewImage().save_images(xy_plot_image)["ui"]["images"]
+
+ # Generate output_images
+ output_images = torch.stack([tensor.squeeze() for tensor in image_tensor_list])
+
+ # Set the output_image the same as plot image defined by 'xyplot_as_output_image'
+ if xyplot_as_output_image == True:
+ output_images = xy_plot_image
+
+ # Print cache if set to true
+ if cache_models == "True":
+ print_loaded_objects_entries(xyplot_id, prompt)
+
+ print("-" * 40) # Print an empty line followed by a separator line
+
+ if sampler_type == "sdxl":
+ sdxl_tuple = original_model, original_clip, original_positive, original_negative,\
+ original_refiner_model, original_refiner_clip, original_refiner_positive, original_refiner_negative
+ result = (sdxl_tuple, latent_list, optional_vae, output_images,)
+ else:
+ result = (original_model, original_positive, original_negative, latent_list, optional_vae, output_images,)
+ return {"ui": {"images": preview_images}, "result": result}
+
+#=======================================================================================================================
+# TSC KSampler Adv (Efficient)
+class TSC_KSamplerAdvanced(TSC_KSampler):
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required":
+ {"model": ("MODEL",),
+ "add_noise": (["enable", "disable"],),
+ "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
+ "positive": ("CONDITIONING",),
+ "negative": ("CONDITIONING",),
+ "latent_image": ("LATENT",),
+ "start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
+ "end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
+ "return_with_leftover_noise": (["disable", "enable"],),
+ "preview_method": (["auto", "latent2rgb", "taesd", "none"],),
+ "vae_decode": (["true", "true (tiled)", "false", "output only", "output only (tiled)"],),
+ },
+ "optional": {"optional_vae": ("VAE",),
+ "script": ("SCRIPT",), },
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", },
+ }
+
+ RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "IMAGE",)
+ RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", "IMAGE",)
+ OUTPUT_NODE = True
+ FUNCTION = "sample_adv"
+ CATEGORY = "Efficiency Nodes/Sampling"
+
+ def sample_adv(self, model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative,
+ latent_image, start_at_step, end_at_step, return_with_leftover_noise, preview_method, vae_decode,
+ prompt=None, extra_pnginfo=None, my_unique_id=None, optional_vae=(None,), script=None):
+
+ return super().sample(model, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative,
+ latent_image, preview_method, vae_decode, denoise=1.0, prompt=prompt, extra_pnginfo=extra_pnginfo, my_unique_id=my_unique_id,
+ optional_vae=optional_vae, script=script, add_noise=add_noise, start_at_step=start_at_step,end_at_step=end_at_step,
+ return_with_leftover_noise=return_with_leftover_noise,sampler_type="advanced")
+
+#=======================================================================================================================
+# TSC KSampler SDXL (Efficient)
+class TSC_KSamplerSDXL(TSC_KSampler):
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required":
+ {"sdxl_tuple": ("SDXL_TUPLE",),
+ "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
+ "latent_image": ("LATENT",),
+ "start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
+ "refine_at_step": ("INT", {"default": -1, "min": -1, "max": 10000}),
+ "preview_method": (["auto", "latent2rgb", "taesd", "none"],),
+ "vae_decode": (["true", "true (tiled)", "false", "output only", "output only (tiled)"],),
+ },
+ "optional": {"optional_vae": ("VAE",),
+ "script": ("SCRIPT",),},
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",},
+ }
+
+ RETURN_TYPES = ("SDXL_TUPLE", "LATENT", "VAE", "IMAGE",)
+ RETURN_NAMES = ("SDXL_TUPLE", "LATENT", "VAE", "IMAGE",)
+ OUTPUT_NODE = True
+ FUNCTION = "sample_sdxl"
+ CATEGORY = "Efficiency Nodes/Sampling"
+
+ def sample_sdxl(self, sdxl_tuple, noise_seed, steps, cfg, sampler_name, scheduler, latent_image,
+ start_at_step, refine_at_step, preview_method, vae_decode, prompt=None, extra_pnginfo=None,
+ my_unique_id=None, optional_vae=(None,), refiner_extras=None, script=None):
+ # sdxl_tuple sent through the 'model' channel
+ negative = None
+ return super().sample(sdxl_tuple, noise_seed, steps, cfg, sampler_name, scheduler,
+ refiner_extras, negative, latent_image, preview_method, vae_decode, denoise=1.0,
+ prompt=prompt, extra_pnginfo=extra_pnginfo, my_unique_id=my_unique_id, optional_vae=optional_vae,
+ script=script, add_noise=None, start_at_step=start_at_step, end_at_step=refine_at_step,
+ return_with_leftover_noise=None,sampler_type="sdxl")
+
+########################################################################################################################
+# Common XY Plot Functions/Variables
+XYPLOT_LIM = 50 #XY Plot default axis size limit
+XYPLOT_DEF = 3 #XY Plot default batch count
+CKPT_EXTENSIONS = LORA_EXTENSIONS = ['.safetensors', '.ckpt']
+VAE_EXTENSIONS = ['.safetensors', '.ckpt', '.pt']
+try:
+ xy_batch_default_path = os.path.abspath(os.sep) + "example_folder"
+except Exception:
+ xy_batch_default_path = ""
+
+def generate_floats(batch_count, first_float, last_float):
+ if batch_count > 1:
+ interval = (last_float - first_float) / (batch_count - 1)
+ return [round(first_float + i * interval, 3) for i in range(batch_count)]
+ else:
+ return [first_float] if batch_count == 1 else []
+
+def generate_ints(batch_count, first_int, last_int):
+ if batch_count > 1:
+ interval = (last_int - first_int) / (batch_count - 1)
+ values = [int(first_int + i * interval) for i in range(batch_count)]
+ else:
+ values = [first_int] if batch_count == 1 else []
+ values = list(set(values)) # Remove duplicates
+ values.sort() # Sort in ascending order
+ return values
+
+def get_batch_files(directory_path, valid_extensions, include_subdirs=False):
+ batch_files = []
+
+ try:
+ if include_subdirs:
+ # Using os.walk to get files from subdirectories
+ for dirpath, dirnames, filenames in os.walk(directory_path):
+ for file in filenames:
+ if any(file.endswith(ext) for ext in valid_extensions):
+ batch_files.append(os.path.join(dirpath, file))
+ else:
+ # Previous code for just the given directory
+ batch_files = [os.path.join(directory_path, f) for f in os.listdir(directory_path) if
+ os.path.isfile(os.path.join(directory_path, f)) and any(
+ f.endswith(ext) for ext in valid_extensions)]
+ except Exception as e:
+ print(f"Error while listing files in {directory_path}: {e}")
+
+ return batch_files
+
+def print_xy_values(xy_type, xy_value, xy_name):
+ print("===== XY Value Returns =====")
+ print(f"{xy_name} Values:")
+ print("- Type:", xy_type)
+ print("- Entries:", xy_value)
+ print("============================")
+
+#=======================================================================================================================
+# TSC XY Plot
+class TSC_XYplot:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "grid_spacing": ("INT", {"default": 0, "min": 0, "max": 500, "step": 5}),
+ "XY_flip": (["False","True"],),
+ "Y_label_orientation": (["Horizontal", "Vertical"],),
+ "cache_models": (["True", "False"],),
+ "ksampler_output_image": (["Images","Plot"],),},
+ "optional": {
+ "dependencies": ("DEPENDENCIES", ),
+ "X": ("XY", ),
+ "Y": ("XY", ),},
+ "hidden": {"my_unique_id": "UNIQUE_ID"},
+ }
+
+ RETURN_TYPES = ("SCRIPT",)
+ RETURN_NAMES = ("SCRIPT",)
+ FUNCTION = "XYplot"
+ CATEGORY = "Efficiency Nodes/Scripts"
+
+ def XYplot(self, grid_spacing, XY_flip, Y_label_orientation, cache_models, ksampler_output_image, my_unique_id,
+ dependencies=None, X=None, Y=None):
+
+ # Unpack X & Y Tuples if connected
+ if X != None:
+ X_type, X_value = X
+ else:
+ X_type = "Nothing"
+ X_value = [""]
+ if Y != None:
+ Y_type, Y_value = Y
+ else:
+ Y_type = "Nothing"
+ Y_value = [""]
+
+ # If types are the same exit. If one isn't "Nothing", print error
+ if X_type != "XY_Capsule" and (X_type == Y_type):
+ if X_type != "Nothing":
+ print(f"{error('XY Plot Error:')} X and Y input types must be different.")
+ return (None,)
+
+ # Check that dependencies are connected for specific plot types
+ encode_types = {
+ "Checkpoint", "Refiner",
+ "LoRA", "LoRA Batch", "LoRA Wt", "LoRA MStr", "LoRA CStr",
+ "Positive Prompt S/R", "Negative Prompt S/R",
+ "AScore+", "AScore-",
+ "Clip Skip", "Clip Skip (Refiner)",
+ "ControlNetStrength", "ControlNetStart%", "ControlNetEnd%"
+ }
+
+ if X_type in encode_types or Y_type in encode_types:
+ if dependencies is None: # Not connected
+ print(f"{error('XY Plot Error:')} The dependencies input must be connected for certain plot types.")
+ # Return None
+ return (None,)
+
+ # Check if both X_type and Y_type are special lora_types
+ lora_types = {"LoRA Batch", "LoRA Wt", "LoRA MStr", "LoRA CStr"}
+ if (X_type in lora_types and Y_type not in lora_types) or (Y_type in lora_types and X_type not in lora_types):
+ print(
+ f"{error('XY Plot Error:')} Both X and Y must be connected to use the 'LoRA Plot' node.")
+ return (None,)
+
+ # Clean Schedulers from Sampler data (if other type is Scheduler)
+ if X_type == "Sampler" and Y_type == "Scheduler":
+ # Clear X_value Scheduler's
+ X_value = [(x[0], "") for x in X_value]
+ elif Y_type == "Sampler" and X_type == "Scheduler":
+ # Clear Y_value Scheduler's
+ Y_value = [(y[0], "") for y in Y_value]
+
+ # Embed information into "Scheduler" X/Y_values for text label
+ if X_type == "Scheduler" and Y_type != "Sampler":
+ # X_value second tuple value of each array entry = None
+ X_value = [(x, None) for x in X_value]
+
+ if Y_type == "Scheduler" and X_type != "Sampler":
+ # Y_value second tuple value of each array entry = None
+ Y_value = [(y, None) for y in Y_value]
+
+ # Clean VAEs from Checkpoint data if other type is VAE
+ if X_type == "Checkpoint" and Y_type == "VAE":
+ # Clear X_value VAE's
+ X_value = [(t[0], t[1], None) for t in X_value]
+ elif Y_type == "VAE" and X_type == "Checkpoint":
+ # Clear Y_value VAE's
+ Y_value = [(t[0], t[1], None) for t in Y_value]
+
+ # Flip X and Y
+ if XY_flip == "True":
+ X_type, Y_type = Y_type, X_type
+ X_value, Y_value = Y_value, X_value
+
+ # Define Ksampler output image behavior
+ xyplot_as_output_image = ksampler_output_image == "Plot"
+
+ # Pack xyplot tuple into its dictionary item under script
+ script = {"xyplot": (X_type, X_value, Y_type, Y_value, grid_spacing, Y_label_orientation, cache_models,
+ xyplot_as_output_image, my_unique_id, dependencies)}
+
+ return (script,)
+
+#=======================================================================================================================
+# TSC XY Plot: Seeds Values
+class TSC_XYplot_SeedsBatch:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),},
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, batch_count):
+ if batch_count == 0:
+ return (None,)
+ xy_type = "Seeds++ Batch"
+ xy_value = list(range(batch_count))
+ return ((xy_type, xy_value),)
+
+#=======================================================================================================================
+# TSC XY Plot: Add/Return Noise
+class TSC_XYplot_AddReturnNoise:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "XY_type": (["add_noise", "return_with_leftover_noise"],)}
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, XY_type):
+ type_mapping = {
+ "add_noise": "AddNoise",
+ "return_with_leftover_noise": "ReturnNoise"
+ }
+ xy_type = type_mapping[XY_type]
+ xy_value = ["enable", "disable"]
+ return ((xy_type, xy_value),)
+
+#=======================================================================================================================
+# TSC XY Plot: Step Values
+class TSC_XYplot_Steps:
+ parameters = ["steps","start_at_step", "end_at_step", "refine_at_step"]
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "target_parameter": (cls.parameters,),
+ "batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "first_step": ("INT", {"default": 10, "min": 1, "max": 10000}),
+ "last_step": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "first_start_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
+ "last_start_step": ("INT", {"default": 10, "min": 0, "max": 10000}),
+ "first_end_step": ("INT", {"default": 10, "min": 0, "max": 10000}),
+ "last_end_step": ("INT", {"default": 20, "min": 0, "max": 10000}),
+ "first_refine_step": ("INT", {"default": 10, "min": 0, "max": 10000}),
+ "last_refine_step": ("INT", {"default": 20, "min": 0, "max": 10000}),
+ }
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, target_parameter, batch_count, first_step, last_step, first_start_step, last_start_step,
+ first_end_step, last_end_step, first_refine_step, last_refine_step):
+
+ if target_parameter == "steps":
+ xy_type = "Steps"
+ xy_first = first_step
+ xy_last = last_step
+ elif target_parameter == "start_at_step":
+ xy_type = "StartStep"
+ xy_first = first_start_step
+ xy_last = last_start_step
+ elif target_parameter == "end_at_step":
+ xy_type = "EndStep"
+ xy_first = first_end_step
+ xy_last = last_end_step
+ elif target_parameter == "refine_at_step":
+ xy_type = "RefineStep"
+ xy_first = first_refine_step
+ xy_last = last_refine_step
+
+ xy_value = generate_ints(batch_count, xy_first, xy_last)
+ return ((xy_type, xy_value),) if xy_value else (None,)
+
+#=======================================================================================================================
+# TSC XY Plot: CFG Values
+class TSC_XYplot_CFG:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "first_cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}),
+ "last_cfg": ("FLOAT", {"default": 9.0, "min": 0.0, "max": 100.0}),
+ }
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, batch_count, first_cfg, last_cfg):
+ xy_type = "CFG Scale"
+ xy_value = generate_floats(batch_count, first_cfg, last_cfg)
+ return ((xy_type, xy_value),) if xy_value else (None,)
+
+#=======================================================================================================================
+# TSC XY Plot: Sampler & Scheduler Values
+class TSC_XYplot_Sampler_Scheduler:
+ parameters = ["sampler", "scheduler", "sampler & scheduler"]
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ samplers = ["None"] + comfy.samplers.KSampler.SAMPLERS
+ schedulers = ["None"] + comfy.samplers.KSampler.SCHEDULERS
+ inputs = {
+ "required": {
+ "target_parameter": (cls.parameters,),
+ "input_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM, "step": 1})
+ }
+ }
+ for i in range(1, XYPLOT_LIM+1):
+ inputs["required"][f"sampler_{i}"] = (samplers,)
+ inputs["required"][f"scheduler_{i}"] = (schedulers,)
+
+ return inputs
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, target_parameter, input_count, **kwargs):
+ if target_parameter == "scheduler":
+ xy_type = "Scheduler"
+ schedulers = [kwargs.get(f"scheduler_{i}") for i in range(1, input_count + 1)]
+ xy_value = [scheduler for scheduler in schedulers if scheduler != "None"]
+ elif target_parameter == "sampler":
+ xy_type = "Sampler"
+ samplers = [kwargs.get(f"sampler_{i}") for i in range(1, input_count + 1)]
+ xy_value = [(sampler, None) for sampler in samplers if sampler != "None"]
+ else:
+ xy_type = "Sampler"
+ samplers = [kwargs.get(f"sampler_{i}") for i in range(1, input_count + 1)]
+ schedulers = [kwargs.get(f"scheduler_{i}") for i in range(1, input_count + 1)]
+ xy_value = [(sampler, scheduler if scheduler != "None" else None) for sampler,
+ scheduler in zip(samplers, schedulers) if sampler != "None"]
+
+ return ((xy_type, xy_value),) if xy_value else (None,)
+
+#=======================================================================================================================
+# TSC XY Plot: Denoise Values
+class TSC_XYplot_Denoise:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "first_denoise": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
+ "last_denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
+ }
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, batch_count, first_denoise, last_denoise):
+ xy_type = "Denoise"
+ xy_value = generate_floats(batch_count, first_denoise, last_denoise)
+ return ((xy_type, xy_value),) if xy_value else (None,)
+
+#=======================================================================================================================
+# TSC XY Plot: VAE Values
+class TSC_XYplot_VAE:
+
+ modes = ["VAE Names", "VAE Batch"]
+
+ @classmethod
+ def INPUT_TYPES(cls):
+
+ vaes = ["None", "Baked VAE"] + folder_paths.get_filename_list("vae")
+
+ inputs = {
+ "required": {
+ "input_mode": (cls.modes,),
+ "batch_path": ("STRING", {"default": xy_batch_default_path, "multiline": False}),
+ "subdirectories": ("BOOLEAN", {"default": False}),
+ "batch_sort": (["ascending", "descending"],),
+ "batch_max": ("INT", {"default": -1, "min": -1, "max": XYPLOT_LIM, "step": 1}),
+ "vae_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM, "step": 1})
+ }
+ }
+
+ for i in range(1, XYPLOT_LIM+1):
+ inputs["required"][f"vae_name_{i}"] = (vaes,)
+
+ return inputs
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, input_mode, batch_path, subdirectories, batch_sort, batch_max, vae_count, **kwargs):
+
+ xy_type = "VAE"
+
+ if "Batch" not in input_mode:
+ # Extract values from kwargs
+ vaes = [kwargs.get(f"vae_name_{i}") for i in range(1, vae_count + 1)]
+ xy_value = [vae for vae in vaes if vae != "None"]
+ else:
+ if batch_max == 0:
+ return (None,)
+
+ try:
+ vaes = get_batch_files(batch_path, VAE_EXTENSIONS, include_subdirs=subdirectories)
+
+ if not vaes:
+ print(f"{error('XY Plot Error:')} No VAE files found.")
+ return (None,)
+
+ if batch_sort == "ascending":
+ vaes.sort()
+ elif batch_sort == "descending":
+ vaes.sort(reverse=True)
+
+ # Construct the xy_value using the obtained vaes
+ xy_value = [vae for vae in vaes]
+
+ if batch_max != -1: # If there's a limit
+ xy_value = xy_value[:batch_max]
+
+ except Exception as e:
+ print(f"{error('XY Plot Error:')} {e}")
+ return (None,)
+
+ return ((xy_type, xy_value),) if xy_value else (None,)
+
+#=======================================================================================================================
+# TSC XY Plot: Prompt S/R
+class TSC_XYplot_PromptSR:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ inputs = {
+ "required": {
+ "target_prompt": (["positive", "negative"],),
+ "search_txt": ("STRING", {"default": "", "multiline": False}),
+ "replace_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM-1}),
+ }
+ }
+
+ # Dynamically add replace_X inputs
+ for i in range(1, XYPLOT_LIM):
+ replace_key = f"replace_{i}"
+ inputs["required"][replace_key] = ("STRING", {"default": "", "multiline": False})
+
+ return inputs
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, target_prompt, search_txt, replace_count, **kwargs):
+ if search_txt == "":
+ return (None,)
+
+ if target_prompt == "positive":
+ xy_type = "Positive Prompt S/R"
+ elif target_prompt == "negative":
+ xy_type = "Negative Prompt S/R"
+
+ # Create base entry
+ xy_values = [(search_txt, None)]
+
+ if replace_count > 0:
+ # Append additional entries based on replace_count
+ xy_values.extend([(search_txt, kwargs.get(f"replace_{i+1}")) for i in range(replace_count)])
+
+ return ((xy_type, xy_values),)
+
+#=======================================================================================================================
+# TSC XY Plot: Aesthetic Score
+class TSC_XYplot_AScore:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "target_ascore": (["positive", "negative"],),
+ "batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "first_ascore": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1000.0, "step": 0.01}),
+ "last_ascore": ("FLOAT", {"default": 10.0, "min": 0.0, "max": 1000.0, "step": 0.01}),
+ }
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, target_ascore, batch_count, first_ascore, last_ascore):
+ if target_ascore == "positive":
+ xy_type = "AScore+"
+ else:
+ xy_type = "AScore-"
+ xy_value = generate_floats(batch_count, first_ascore, last_ascore)
+ return ((xy_type, xy_value),) if xy_value else (None,)
+
+#=======================================================================================================================
+# TSC XY Plot: Refiner On/Off
+class TSC_XYplot_Refiner_OnOff:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "refine_at_percent": ("FLOAT",{"default": 0.80, "min": 0.00, "max": 1.00, "step": 0.01})},
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, refine_at_percent):
+ xy_type = "Refiner On/Off"
+ xy_value = [refine_at_percent, 1]
+ return ((xy_type, xy_value),)
+
+#=======================================================================================================================
+# TSC XY Plot: Clip Skip
+class TSC_XYplot_ClipSkip:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "target_ckpt": (["Base","Refiner"],),
+ "batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "first_clip_skip": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}),
+ "last_clip_skip": ("INT", {"default": -3, "min": -24, "max": -1, "step": 1}),
+ },
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, target_ckpt, batch_count, first_clip_skip, last_clip_skip):
+ if target_ckpt == "Base":
+ xy_type = "Clip Skip"
+ else:
+ xy_type = "Clip Skip (Refiner)"
+ xy_value = generate_ints(batch_count, first_clip_skip, last_clip_skip)
+ return ((xy_type, xy_value),) if xy_value else (None,)
+
+#=======================================================================================================================
+# TSC XY Plot: Checkpoint Values
+class TSC_XYplot_Checkpoint:
+ modes = ["Ckpt Names", "Ckpt Names+ClipSkip", "Ckpt Names+ClipSkip+VAE", "Checkpoint Batch"]
+ @classmethod
+ def INPUT_TYPES(cls):
+ checkpoints = ["None"] + folder_paths.get_filename_list("checkpoints")
+ vaes = ["Baked VAE"] + folder_paths.get_filename_list("vae")
+
+ inputs = {
+ "required": {
+ "target_ckpt": (["Base", "Refiner"],),
+ "input_mode": (cls.modes,),
+ "batch_path": ("STRING", {"default": xy_batch_default_path, "multiline": False}),
+ "subdirectories": ("BOOLEAN", {"default": False}),
+ "batch_sort": (["ascending", "descending"],),
+ "batch_max": ("INT", {"default": -1, "min": -1, "max": 50, "step": 1}),
+ "ckpt_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM, "step": 1})
+ }
+ }
+
+ for i in range(1, XYPLOT_LIM+1):
+ inputs["required"][f"ckpt_name_{i}"] = (checkpoints,)
+ inputs["required"][f"clip_skip_{i}"] = ("INT", {"default": -1, "min": -24, "max": -1, "step": 1})
+ inputs["required"][f"vae_name_{i}"] = (vaes,)
+
+ return inputs
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, target_ckpt, input_mode, batch_path, subdirectories, batch_sort, batch_max, ckpt_count, **kwargs):
+
+ # Define XY type
+ xy_type = "Checkpoint" if target_ckpt == "Base" else "Refiner"
+
+ if "Batch" not in input_mode:
+ # Extract values from kwargs
+ checkpoints = [kwargs.get(f"ckpt_name_{i}") for i in range(1, ckpt_count + 1)]
+ clip_skips = [kwargs.get(f"clip_skip_{i}") for i in range(1, ckpt_count + 1)]
+ vaes = [kwargs.get(f"vae_name_{i}") for i in range(1, ckpt_count + 1)]
+
+ # Set None for Clip Skip and/or VAE if not correct modes
+ for i in range(ckpt_count):
+ if "ClipSkip" not in input_mode:
+ clip_skips[i] = None
+ if "VAE" not in input_mode:
+ vaes[i] = None
+
+ xy_value = [(checkpoint, clip_skip, vae) for checkpoint, clip_skip, vae in zip(checkpoints, clip_skips, vaes) if
+ checkpoint != "None"]
+ else:
+ if batch_max == 0:
+ return (None,)
+
+ try:
+ ckpts = get_batch_files(batch_path, CKPT_EXTENSIONS, include_subdirs=subdirectories)
+
+ if not ckpts:
+ print(f"{error('XY Plot Error:')} No Checkpoint files found.")
+ return (None,)
+
+ if batch_sort == "ascending":
+ ckpts.sort()
+ elif batch_sort == "descending":
+ ckpts.sort(reverse=True)
+
+ # Construct the xy_value using the obtained ckpts
+ xy_value = [(ckpt, None, None) for ckpt in ckpts]
+
+ if batch_max != -1: # If there's a limit
+ xy_value = xy_value[:batch_max]
+
+ except Exception as e:
+ print(f"{error('XY Plot Error:')} {e}")
+ return (None,)
+
+ return ((xy_type, xy_value),) if xy_value else (None,)
+
+#=======================================================================================================================
+# TSC XY Plot: LoRA Batch (DISABLED)
+class TSC_XYplot_LoRA_Batch:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+
+ return {"required": {
+ "batch_path": ("STRING", {"default": xy_batch_default_path, "multiline": False}),
+ "subdirectories": ("BOOLEAN", {"default": False}),
+ "batch_sort": (["ascending", "descending"],),
+ "batch_max": ("INT",{"default": -1, "min": -1, "max": XYPLOT_LIM, "step": 1}),
+ "model_strength": ("FLOAT", {"default": 1.0, "min": -10.00, "max": 10.0, "step": 0.01}),
+ "clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})},
+ "optional": {"lora_stack": ("LORA_STACK",)}
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, batch_path, subdirectories, batch_sort, model_strength, clip_strength, batch_max, lora_stack=None):
+ if batch_max == 0:
+ return (None,)
+
+ xy_type = "LoRA"
+
+ loras = get_batch_files(batch_path, LORA_EXTENSIONS, include_subdirs=subdirectories)
+
+ if not loras:
+ print(f"{error('XY Plot Error:')} No LoRA files found.")
+ return (None,)
+
+ if batch_sort == "ascending":
+ loras.sort()
+ elif batch_sort == "descending":
+ loras.sort(reverse=True)
+
+ # Construct the xy_value using the obtained loras
+ xy_value = [[(lora, model_strength, clip_strength)] + (lora_stack if lora_stack else []) for lora in loras]
+
+ if batch_max != -1: # If there's a limit
+ xy_value = xy_value[:batch_max]
+
+ return ((xy_type, xy_value),) if xy_value else (None,)
+
+#=======================================================================================================================
+# TSC XY Plot: LoRA Values
+class TSC_XYplot_LoRA:
+ modes = ["LoRA Names", "LoRA Names+Weights", "LoRA Batch"]
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ loras = ["None"] + folder_paths.get_filename_list("loras")
+
+ inputs = {
+ "required": {
+ "input_mode": (cls.modes,),
+ "batch_path": ("STRING", {"default": xy_batch_default_path, "multiline": False}),
+ "subdirectories": ("BOOLEAN", {"default": False}),
+ "batch_sort": (["ascending", "descending"],),
+ "batch_max": ("INT", {"default": -1, "min": -1, "max": XYPLOT_LIM, "step": 1}),
+ "lora_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM, "step": 1}),
+ "model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ }
+ }
+
+ for i in range(1, XYPLOT_LIM+1):
+ inputs["required"][f"lora_name_{i}"] = (loras,)
+ inputs["required"][f"model_str_{i}"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
+ inputs["required"][f"clip_str_{i}"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
+
+ inputs["optional"] = {
+ "lora_stack": ("LORA_STACK",)
+ }
+ return inputs
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def __init__(self):
+ self.lora_batch = TSC_XYplot_LoRA_Batch()
+
+ def xy_value(self, input_mode, batch_path, subdirectories, batch_sort, batch_max, lora_count, model_strength,
+ clip_strength, lora_stack=None, **kwargs):
+
+ xy_type = "LoRA"
+ result = (None,)
+ lora_stack = lora_stack if lora_stack else []
+
+ if "Batch" not in input_mode:
+ # Extract values from kwargs
+ loras = [kwargs.get(f"lora_name_{i}") for i in range(1, lora_count + 1)]
+ model_strs = [kwargs.get(f"model_str_{i}", model_strength) for i in range(1, lora_count + 1)]
+ clip_strs = [kwargs.get(f"clip_str_{i}", clip_strength) for i in range(1, lora_count + 1)]
+
+ # Use model_strength and clip_strength for the loras where values are not provided
+ if "Weights" not in input_mode:
+ for i in range(lora_count):
+ model_strs[i] = model_strength
+ clip_strs[i] = clip_strength
+
+ # Extend each sub-array with lora_stack if it's not None
+ xy_value = [[(lora, model_str, clip_str)] + lora_stack for lora, model_str, clip_str
+ in zip(loras, model_strs, clip_strs) if lora != "None"]
+
+ result = ((xy_type, xy_value),)
+ else:
+ try:
+ result = self.lora_batch.xy_value(batch_path, subdirectories, batch_sort, model_strength,
+ clip_strength, batch_max, lora_stack)
+ except Exception as e:
+ print(f"{error('XY Plot Error:')} {e}")
+
+ return result
+
+#=======================================================================================================================
+# TSC XY Plot: LoRA Plot
+class TSC_XYplot_LoRA_Plot:
+
+ modes = ["X: LoRA Batch, Y: LoRA Weight",
+ "X: LoRA Batch, Y: Model Strength",
+ "X: LoRA Batch, Y: Clip Strength",
+ "X: Model Strength, Y: Clip Strength",
+ ]
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ loras = ["None"] + folder_paths.get_filename_list("loras")
+ return {"required": {
+ "input_mode": (cls.modes,),
+ "lora_name": (loras,),
+ "model_strength": ("FLOAT", {"default": 1.0, "min": -10.00, "max": 10.0, "step": 0.01}),
+ "clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "X_batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "X_batch_path": ("STRING", {"default": xy_batch_default_path, "multiline": False}),
+ "X_subdirectories": ("BOOLEAN", {"default": False}),
+ "X_batch_sort": (["ascending", "descending"],),
+ "X_first_value": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "X_last_value": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "Y_batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "Y_first_value": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "Y_last_value": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),},
+ "optional": {"lora_stack": ("LORA_STACK",)}
+ }
+
+ RETURN_TYPES = ("XY","XY",)
+ RETURN_NAMES = ("X","Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def __init__(self):
+ self.lora_batch = TSC_XYplot_LoRA_Batch()
+
+ def generate_values(self, mode, X_or_Y, *args, **kwargs):
+ result = self.lora_batch.xy_value(*args, **kwargs)
+
+ if result and result[0]:
+ xy_type, xy_value_list = result[0]
+
+ # Adjust type based on the mode
+ if "LoRA Weight" in mode:
+ xy_type = "LoRA Wt"
+ elif "Model Strength" in mode:
+ xy_type = "LoRA MStr"
+ elif "Clip Strength" in mode:
+ xy_type = "LoRA CStr"
+
+ # Check whether the value is for X or Y
+ if "LoRA Batch" in mode: # Changed condition
+ return self.generate_batch_values(*args, **kwargs)
+ else:
+ return ((xy_type, xy_value_list),)
+
+ return (None,)
+
+ def xy_value(self, input_mode, lora_name, model_strength, clip_strength, X_batch_count, X_batch_path, X_subdirectories,
+ X_batch_sort, X_first_value, X_last_value, Y_batch_count, Y_first_value, Y_last_value, lora_stack=None):
+
+ x_value, y_value = [], []
+ lora_stack = lora_stack if lora_stack else []
+
+ if "Model Strength" in input_mode and "Clip Strength" in input_mode:
+ if lora_name == 'None':
+ return (None,None,)
+ if "LoRA Batch" in input_mode:
+ lora_name = None
+ if "LoRA Weight" in input_mode:
+ model_strength = None
+ clip_strength = None
+ if "Model Strength" in input_mode:
+ model_strength = None
+ if "Clip Strength" in input_mode:
+ clip_strength = None
+
+ # Handling X values
+ if "X: LoRA Batch" in input_mode:
+ try:
+ x_value = self.lora_batch.xy_value(X_batch_path, X_subdirectories, X_batch_sort,
+ model_strength, clip_strength, X_batch_count, lora_stack)[0][1]
+ except Exception as e:
+ print(f"{error('XY Plot Error:')} {e}")
+ return (None,)
+ x_type = "LoRA Batch"
+ elif "X: Model Strength" in input_mode:
+ x_floats = generate_floats(X_batch_count, X_first_value, X_last_value)
+ x_type = "LoRA MStr"
+ x_value = [[(lora_name, x, clip_strength)] + lora_stack for x in x_floats]
+
+ # Handling Y values
+ y_floats = generate_floats(Y_batch_count, Y_first_value, Y_last_value)
+ if "Y: LoRA Weight" in input_mode:
+ y_type = "LoRA Wt"
+ y_value = [[(lora_name, y, y)] + lora_stack for y in y_floats]
+ elif "Y: Model Strength" in input_mode:
+ y_type = "LoRA MStr"
+ y_value = [[(lora_name, y, clip_strength)] + lora_stack for y in y_floats]
+ elif "Y: Clip Strength" in input_mode:
+ y_type = "LoRA CStr"
+ y_value = [[(lora_name, model_strength, y)] + lora_stack for y in y_floats]
+
+ return ((x_type, x_value), (y_type, y_value))
+
+#=======================================================================================================================
+# TSC XY Plot: LoRA Stacks
+class TSC_XYplot_LoRA_Stacks:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "node_state": (["Enabled"],)},
+ "optional": {
+ "lora_stack_1": ("LORA_STACK",),
+ "lora_stack_2": ("LORA_STACK",),
+ "lora_stack_3": ("LORA_STACK",),
+ "lora_stack_4": ("LORA_STACK",),
+ "lora_stack_5": ("LORA_STACK",),},
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, node_state, lora_stack_1=None, lora_stack_2=None, lora_stack_3=None, lora_stack_4=None, lora_stack_5=None):
+ xy_type = "LoRA"
+ xy_value = [stack for stack in [lora_stack_1, lora_stack_2, lora_stack_3, lora_stack_4, lora_stack_5] if stack is not None]
+ if not xy_value or not any(xy_value) or node_state == "Disabled":
+ return (None,)
+ else:
+ return ((xy_type, xy_value),)
+
+#=======================================================================================================================
+# TSC XY Plot: Control Net Strength (DISABLED)
+class TSC_XYplot_Control_Net_Strength:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "control_net": ("CONTROL_NET",),
+ "image": ("IMAGE",),
+ "batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "first_strength": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "last_strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ },
+ "optional": {"cnet_stack": ("CONTROL_NET_STACK",)},
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, control_net, image, batch_count, first_strength, last_strength,
+ start_percent, end_percent, cnet_stack=None):
+
+ if batch_count == 0:
+ return (None,)
+
+ xy_type = "ControlNetStrength"
+ strength_increment = (last_strength - first_strength) / (batch_count - 1) if batch_count > 1 else 0
+
+ xy_value = []
+
+ # Always add the first strength.
+ xy_value.append([(control_net, image, first_strength, start_percent, end_percent)])
+
+ # Add intermediate strengths only if batch_count is more than 2.
+ for i in range(1, batch_count - 1):
+ xy_value.append([(control_net, image, first_strength + i * strength_increment, start_percent,
+ end_percent)])
+
+ # Always add the last strength if batch_count is more than 1.
+ if batch_count > 1:
+ xy_value.append([(control_net, image, last_strength, start_percent, end_percent)])
+
+ # If cnet_stack is provided, extend each inner array with its content
+ if cnet_stack:
+ for inner_list in xy_value:
+ inner_list.extend(cnet_stack)
+
+ return ((xy_type, xy_value),)
+
+#=======================================================================================================================
+# TSC XY Plot: Control Net Start % (DISABLED)
+class TSC_XYplot_Control_Net_Start:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "control_net": ("CONTROL_NET",),
+ "image": ("IMAGE",),
+ "batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "first_start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "last_start_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ },
+ "optional": {"cnet_stack": ("CONTROL_NET_STACK",)},
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, control_net, image, batch_count, first_start_percent, last_start_percent,
+ strength, end_percent, cnet_stack=None):
+
+ if batch_count == 0:
+ return (None,)
+
+ xy_type = "ControlNetStart%"
+ percent_increment = (last_start_percent - first_start_percent) / (batch_count - 1) if batch_count > 1 else 0
+
+ xy_value = []
+
+ # Always add the first start_percent.
+ xy_value.append([(control_net, image, strength, first_start_percent, end_percent)])
+
+ # Add intermediate start percents only if batch_count is more than 2.
+ for i in range(1, batch_count - 1):
+ xy_value.append([(control_net, image, strength, first_start_percent + i * percent_increment,
+ end_percent)])
+
+ # Always add the last start_percent if batch_count is more than 1.
+ if batch_count > 1:
+ xy_value.append([(control_net, image, strength, last_start_percent, end_percent)])
+
+ # If cnet_stack is provided, extend each inner array with its content
+ if cnet_stack:
+ for inner_list in xy_value:
+ inner_list.extend(cnet_stack)
+
+ return ((xy_type, xy_value),)
+
+#=======================================================================================================================
+# TSC XY Plot: Control Net End % (DISABLED)
+class TSC_XYplot_Control_Net_End:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "control_net": ("CONTROL_NET",),
+ "image": ("IMAGE",),
+ "batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "first_end_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "last_end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "start_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ },
+ "optional": {"cnet_stack": ("CONTROL_NET_STACK",)},
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, control_net, image, batch_count, first_end_percent, last_end_percent,
+ strength, start_percent, cnet_stack=None):
+
+ if batch_count == 0:
+ return (None,)
+
+ xy_type = "ControlNetEnd%"
+ percent_increment = (last_end_percent - first_end_percent) / (batch_count - 1) if batch_count > 1 else 0
+
+ xy_value = []
+
+ # Always add the first end_percent.
+ xy_value.append([(control_net, image, strength, start_percent, first_end_percent)])
+
+ # Add intermediate end percents only if batch_count is more than 2.
+ for i in range(1, batch_count - 1):
+ xy_value.append([(control_net, image, strength, start_percent,
+ first_end_percent + i * percent_increment)])
+
+ # Always add the last end_percent if batch_count is more than 1.
+ if batch_count > 1:
+ xy_value.append([(control_net, image, strength, start_percent, last_end_percent)])
+
+ # If cnet_stack is provided, extend each inner array with its content
+ if cnet_stack:
+ for inner_list in xy_value:
+ inner_list.extend(cnet_stack)
+
+ return ((xy_type, xy_value),)
+
+
+# =======================================================================================================================
+# TSC XY Plot: Control Net
+class TSC_XYplot_Control_Net(TSC_XYplot_Control_Net_Strength, TSC_XYplot_Control_Net_Start, TSC_XYplot_Control_Net_End):
+ parameters = ["strength", "start_percent", "end_percent"]
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "control_net": ("CONTROL_NET",),
+ "image": ("IMAGE",),
+ "target_parameter": (cls.parameters,),
+ "batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "first_strength": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "last_strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "first_start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "last_start_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "first_end_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "last_end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ },
+ "optional": {"cnet_stack": ("CONTROL_NET_STACK",)},
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, control_net, image, target_parameter, batch_count, first_strength, last_strength, first_start_percent,
+ last_start_percent, first_end_percent, last_end_percent, strength, start_percent, end_percent, cnet_stack=None):
+
+ if target_parameter == "strength":
+ return TSC_XYplot_Control_Net_Strength.xy_value(self, control_net, image, batch_count, first_strength,
+ last_strength, start_percent, end_percent, cnet_stack=cnet_stack)
+ elif target_parameter == "start_percent":
+ return TSC_XYplot_Control_Net_Start.xy_value(self, control_net, image, batch_count, first_start_percent,
+ last_start_percent, strength, end_percent, cnet_stack=cnet_stack)
+ elif target_parameter == "end_percent":
+ return TSC_XYplot_Control_Net_End.xy_value(self, control_net, image, batch_count, first_end_percent,
+ last_end_percent, strength, start_percent, cnet_stack=cnet_stack)
+
+#=======================================================================================================================
+# TSC XY Plot: Control Net Plot
+class TSC_XYplot_Control_Net_Plot:
+
+ plot_types = ["X: Strength, Y: Start%",
+ "X: Strength, Y: End%",
+ "X: Start%, Y: Strength",
+ "X: Start%, Y: End%",
+ "X: End%, Y: Strength",
+ "X: End%, Y: Start%"]
+
+ @classmethod
+ def INPUT_TYPES(cls):
+
+ return {
+ "required": {
+ "control_net": ("CONTROL_NET",),
+ "image": ("IMAGE",),
+ "plot_type": (cls.plot_types,),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
+ "X_batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "X_first_value": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "X_last_value": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "Y_batch_count": ("INT", {"default": XYPLOT_DEF, "min": 0, "max": XYPLOT_LIM}),
+ "Y_first_value": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 10.0, "step": 0.01}),
+ "Y_last_value": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),},
+ "optional": {"cnet_stack": ("CONTROL_NET_STACK",)},
+ }
+
+ RETURN_TYPES = ("XY","XY",)
+ RETURN_NAMES = ("X","Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def get_value(self, axis, control_net, image, strength, start_percent, end_percent,
+ batch_count, first_value, last_value):
+
+ # Adjust upper bound for Start% and End% type
+ if axis in ["Start%", "End%"]:
+ first_value = min(1, first_value)
+ last_value = min(1, last_value)
+
+ increment = (last_value - first_value) / (batch_count - 1) if batch_count > 1 else 0
+
+ values = []
+
+ # Always add the first value.
+ if axis == "Strength":
+ values.append([(control_net, image, first_value, start_percent, end_percent)])
+ elif axis == "Start%":
+ values.append([(control_net, image, strength, first_value, end_percent)])
+ elif axis == "End%":
+ values.append([(control_net, image, strength, start_percent, first_value)])
+
+ # Add intermediate values only if batch_count is more than 2.
+ for i in range(1, batch_count - 1):
+ if axis == "Strength":
+ values.append(
+ [(control_net, image, first_value + i * increment, start_percent, end_percent)])
+ elif axis == "Start%":
+ values.append(
+ [(control_net, image, strength, first_value + i * increment, end_percent)])
+ elif axis == "End%":
+ values.append(
+ [(control_net, image, strength, start_percent, first_value + i * increment)])
+
+ # Always add the last value if batch_count is more than 1.
+ if batch_count > 1:
+ if axis == "Strength":
+ values.append([(control_net, image, last_value, start_percent, end_percent)])
+ elif axis == "Start%":
+ values.append([(control_net, image, strength, last_value, end_percent)])
+ elif axis == "End%":
+ values.append([(control_net, image, strength, start_percent, last_value)])
+
+ return values
+
+ def xy_value(self, control_net, image, strength, start_percent, end_percent, plot_type,
+ X_batch_count, X_first_value, X_last_value, Y_batch_count, Y_first_value, Y_last_value,
+ cnet_stack=None):
+
+ x_type, y_type = plot_type.split(", ")
+
+ # Now split each type by ": "
+ x_type = x_type.split(": ")[1].strip()
+ y_type = y_type.split(": ")[1].strip()
+
+ x_entry = None
+ y_entry = None
+
+ if X_batch_count > 0:
+ x_value = self.get_value(x_type, control_net, image, strength, start_percent,
+ end_percent, X_batch_count, X_first_value, X_last_value)
+ # If cnet_stack is provided, extend each inner array with its content
+ if cnet_stack:
+ for inner_list in x_value:
+ inner_list.extend(cnet_stack)
+
+ x_entry = ("ControlNet" + x_type, x_value)
+
+ if Y_batch_count > 0:
+ y_value = self.get_value(y_type, control_net, image, strength, start_percent,
+ end_percent, Y_batch_count, Y_first_value, Y_last_value)
+ # If cnet_stack is provided, extend each inner array with its content
+ if cnet_stack:
+ for inner_list in y_value:
+ inner_list.extend(cnet_stack)
+
+ y_entry = ("ControlNet" + y_type, y_value)
+
+ return (x_entry, y_entry,)
+
+#=======================================================================================================================
+# TSC XY Plot: Manual Entry Notes
+class TSC_XYplot_Manual_XY_Entry_Info:
+
+ syntax = "(X/Y_types) (X/Y_values)\n" \
+ "Seeds++ Batch batch_count\n" \
+ "Steps steps_1;steps_2;...\n" \
+ "StartStep start_step_1;start_step_2;...\n" \
+ "EndStep end_step_1;end_step_2;...\n" \
+ "CFG Scale cfg_1;cfg_2;...\n" \
+ "Sampler(1) sampler_1;sampler_2;...\n" \
+ "Sampler(2) sampler_1,scheduler_1;...\n" \
+ "Sampler(3) sampler_1;...;,default_scheduler\n" \
+ "Scheduler scheduler_1;scheduler_2;...\n" \
+ "Denoise denoise_1;denoise_2;...\n" \
+ "VAE vae_1;vae_2;vae_3;...\n" \
+ "+Prompt S/R search_txt;replace_1;replace_2;...\n" \
+ "-Prompt S/R search_txt;replace_1;replace_2;...\n" \
+ "Checkpoint(1) ckpt_1;ckpt_2;ckpt_3;...\n" \
+ "Checkpoint(2) ckpt_1,clip_skip_1;...\n" \
+ "Checkpoint(3) ckpt_1;ckpt_2;...;,default_clip_skip\n" \
+ "Clip Skip clip_skip_1;clip_skip_2;...\n" \
+ "LoRA(1) lora_1;lora_2;lora_3;...\n" \
+ "LoRA(2) lora_1;...;,default_model_str,default_clip_str\n" \
+ "LoRA(3) lora_1,model_str_1,clip_str_1;..."
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ samplers = ";\n".join(comfy.samplers.KSampler.SAMPLERS)
+ schedulers = ";\n".join(comfy.samplers.KSampler.SCHEDULERS)
+ vaes = ";\n".join(folder_paths.get_filename_list("vae"))
+ ckpts = ";\n".join(folder_paths.get_filename_list("checkpoints"))
+ loras = ";\n".join(folder_paths.get_filename_list("loras"))
+ return {"required": {
+ "notes": ("STRING", {"default":
+ f"_____________SYNTAX_____________\n{cls.syntax}\n\n"
+ f"____________SAMPLERS____________\n{samplers}\n\n"
+ f"___________SCHEDULERS___________\n{schedulers}\n\n"
+ f"_____________VAES_______________\n{vaes}\n\n"
+ f"___________CHECKPOINTS__________\n{ckpts}\n\n"
+ f"_____________LORAS______________\n{loras}\n","multiline": True}),},}
+
+ RETURN_TYPES = ()
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+
+#=======================================================================================================================
+# TSC XY Plot: Manual Entry
+class TSC_XYplot_Manual_XY_Entry:
+
+ plot_types = ["Nothing", "Seeds++ Batch", "Steps", "StartStep", "EndStep", "CFG Scale", "Sampler", "Scheduler",
+ "Denoise", "VAE", "Positive Prompt S/R", "Negative Prompt S/R", "Checkpoint", "Clip Skip", "LoRA"]
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "plot_type": (cls.plot_types,),
+ "plot_value": ("STRING", {"default": "", "multiline": True}),}
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, plot_type, plot_value):
+
+ # Store X values as arrays
+ if plot_type not in {"Positive Prompt S/R", "Negative Prompt S/R", "VAE", "Checkpoint", "LoRA"}:
+ plot_value = plot_value.replace(" ", "") # Remove spaces
+ plot_value = plot_value.replace("\n", "") # Remove newline characters
+ plot_value = plot_value.rstrip(";") # Remove trailing semicolon
+ plot_value = plot_value.split(";") # Turn to array
+
+ # Define the valid bounds for each type
+ bounds = {
+ "Seeds++ Batch": {"min": 1, "max": 50},
+ "Steps": {"min": 1, "max": 10000},
+ "StartStep": {"min": 0, "max": 10000},
+ "EndStep": {"min": 0, "max": 10000},
+ "CFG Scale": {"min": 0, "max": 100},
+ "Sampler": {"options": comfy.samplers.KSampler.SAMPLERS},
+ "Scheduler": {"options": comfy.samplers.KSampler.SCHEDULERS},
+ "Denoise": {"min": 0, "max": 1},
+ "VAE": {"options": folder_paths.get_filename_list("vae")},
+ "Checkpoint": {"options": folder_paths.get_filename_list("checkpoints")},
+ "Clip Skip": {"min": -24, "max": -1},
+ "LoRA": {"options": folder_paths.get_filename_list("loras"),
+ "model_str": {"min": -10, "max": 10},"clip_str": {"min": -10, "max": 10},},
+ }
+
+ # Validates a value based on its corresponding value_type and bounds.
+ def validate_value(value, value_type, bounds):
+ # ________________________________________________________________________
+ # Seeds++ Batch
+ if value_type == "Seeds++ Batch":
+ try:
+ x = int(float(value))
+ if x < bounds["Seeds++ Batch"]["min"]:
+ x = bounds["Seeds++ Batch"]["min"]
+ elif x > bounds["Seeds++ Batch"]["max"]:
+ x = bounds["Seeds++ Batch"]["max"]
+ except ValueError:
+ print(f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid batch count.")
+ return None
+ if float(value) != x:
+ print(f"\033[31mmXY Plot Error:\033[0m '{value}' is not a valid batch count.")
+ return None
+ return x
+ # ________________________________________________________________________
+ # Steps
+ elif value_type == "Steps":
+ try:
+ x = int(value)
+ if x < bounds["Steps"]["min"]:
+ x = bounds["Steps"]["min"]
+ elif x > bounds["Steps"]["max"]:
+ x = bounds["Steps"]["max"]
+ return x
+ except ValueError:
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid Step count.")
+ return None
+ # __________________________________________________________________________________________________________
+ # Start at Step
+ elif value_type == "StartStep":
+ try:
+ x = int(value)
+ if x < bounds["StartStep"]["min"]:
+ x = bounds["StartStep"]["min"]
+ elif x > bounds["StartStep"]["max"]:
+ x = bounds["StartStep"]["max"]
+ return x
+ except ValueError:
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid Start Step.")
+ return None
+ # __________________________________________________________________________________________________________
+ # End at Step
+ elif value_type == "EndStep":
+ try:
+ x = int(value)
+ if x < bounds["EndStep"]["min"]:
+ x = bounds["EndStep"]["min"]
+ elif x > bounds["EndStep"]["max"]:
+ x = bounds["EndStep"]["max"]
+ return x
+ except ValueError:
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid End Step.")
+ return None
+ # __________________________________________________________________________________________________________
+ # CFG Scale
+ elif value_type == "CFG Scale":
+ try:
+ x = float(value)
+ if x < bounds["CFG Scale"]["min"]:
+ x = bounds["CFG Scale"]["min"]
+ elif x > bounds["CFG Scale"]["max"]:
+ x = bounds["CFG Scale"]["max"]
+ return x
+ except ValueError:
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{value}' is not a number between {bounds['CFG Scale']['min']}"
+ f" and {bounds['CFG Scale']['max']} for CFG Scale.")
+ return None
+ # __________________________________________________________________________________________________________
+ # Sampler
+ elif value_type == "Sampler":
+ if isinstance(value, str) and ',' in value:
+ value = tuple(map(str.strip, value.split(',')))
+ if isinstance(value, tuple):
+ if len(value) >= 2:
+ value = value[:2] # Slice the value tuple to keep only the first two elements
+ sampler, scheduler = value
+ scheduler = scheduler.lower() # Convert the scheduler name to lowercase
+ if sampler not in bounds["Sampler"]["options"]:
+ valid_samplers = '\n'.join(bounds["Sampler"]["options"])
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{sampler}' is not a valid sampler. Valid samplers are:\n{valid_samplers}")
+ sampler = None
+ if scheduler not in bounds["Scheduler"]["options"]:
+ valid_schedulers = '\n'.join(bounds["Scheduler"]["options"])
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{scheduler}' is not a valid scheduler. Valid schedulers are:\n{valid_schedulers}")
+ scheduler = None
+ if sampler is None or scheduler is None:
+ return None
+ else:
+ return sampler, scheduler
+ else:
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid sampler.'")
+ return None
+ else:
+ if value not in bounds["Sampler"]["options"]:
+ valid_samplers = '\n'.join(bounds["Sampler"]["options"])
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid sampler. Valid samplers are:\n{valid_samplers}")
+ return None
+ else:
+ return value, None
+ # __________________________________________________________________________________________________________
+ # Scheduler
+ elif value_type == "Scheduler":
+ if value not in bounds["Scheduler"]["options"]:
+ valid_schedulers = '\n'.join(bounds["Scheduler"]["options"])
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid Scheduler. Valid Schedulers are:\n{valid_schedulers}")
+ return None
+ else:
+ return value
+ # __________________________________________________________________________________________________________
+ # Denoise
+ elif value_type == "Denoise":
+ try:
+ x = float(value)
+ if x < bounds["Denoise"]["min"]:
+ x = bounds["Denoise"]["min"]
+ elif x > bounds["Denoise"]["max"]:
+ x = bounds["Denoise"]["max"]
+ return x
+ except ValueError:
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{value}' is not a number between {bounds['Denoise']['min']} "
+ f"and {bounds['Denoise']['max']} for Denoise.")
+ return None
+ # __________________________________________________________________________________________________________
+ # VAE
+ elif value_type == "VAE":
+ if value not in bounds["VAE"]["options"]:
+ valid_vaes = '\n'.join(bounds["VAE"]["options"])
+ print(f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid VAE. Valid VAEs are:\n{valid_vaes}")
+ return None
+ else:
+ return value
+ # __________________________________________________________________________________________________________
+ # Checkpoint
+ elif value_type == "Checkpoint":
+ if isinstance(value, str) and ',' in value:
+ value = tuple(map(str.strip, value.split(',')))
+ if isinstance(value, tuple):
+ if len(value) >= 2:
+ value = value[:2] # Slice the value tuple to keep only the first two elements
+ checkpoint, clip_skip = value
+ try:
+ clip_skip = int(clip_skip) # Convert the clip_skip to integer
+ except ValueError:
+ print(f"\033[31mXY Plot Error:\033[0m '{clip_skip}' is not a valid clip_skip. "
+ f"Valid clip skip values are integers between {bounds['Clip Skip']['min']} and {bounds['Clip Skip']['max']}.")
+ return None
+ if checkpoint not in bounds["Checkpoint"]["options"]:
+ valid_checkpoints = '\n'.join(bounds["Checkpoint"]["options"])
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{checkpoint}' is not a valid checkpoint. Valid checkpoints are:\n{valid_checkpoints}")
+ checkpoint = None
+ if clip_skip < bounds["Clip Skip"]["min"] or clip_skip > bounds["Clip Skip"]["max"]:
+ print(f"\033[31mXY Plot Error:\033[0m '{clip_skip}' is not a valid clip skip. "
+ f"Valid clip skip values are integers between {bounds['Clip Skip']['min']} and {bounds['Clip Skip']['max']}.")
+ clip_skip = None
+ if checkpoint is None or clip_skip is None:
+ return None
+ else:
+ return checkpoint, clip_skip, None
+ else:
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid checkpoint.'")
+ return None
+ else:
+ if value not in bounds["Checkpoint"]["options"]:
+ valid_checkpoints = '\n'.join(bounds["Checkpoint"]["options"])
+ print(
+ f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid checkpoint. Valid checkpoints are:\n{valid_checkpoints}")
+ return None
+ else:
+ return value, None, None
+ # __________________________________________________________________________________________________________
+ # Clip Skip
+ elif value_type == "Clip Skip":
+ try:
+ x = int(value)
+ if x < bounds["Clip Skip"]["min"]:
+ x = bounds["Clip Skip"]["min"]
+ elif x > bounds["Clip Skip"]["max"]:
+ x = bounds["Clip Skip"]["max"]
+ return x
+ except ValueError:
+ print(f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid Clip Skip.")
+ return None
+ # __________________________________________________________________________________________________________
+ # LoRA
+ elif value_type == "LoRA":
+ if isinstance(value, str) and ',' in value:
+ value = tuple(map(str.strip, value.split(',')))
+
+ if isinstance(value, tuple):
+ lora_name, model_str, clip_str = (value + (1.0, 1.0))[:3] # Defaults model_str and clip_str to 1 if not provided
+
+ if lora_name not in bounds["LoRA"]["options"]:
+ valid_loras = '\n'.join(bounds["LoRA"]["options"])
+ print(f"{error('XY Plot Error:')} '{lora_name}' is not a valid LoRA. Valid LoRAs are:\n{valid_loras}")
+ lora_name = None
+
+ try:
+ model_str = float(model_str)
+ clip_str = float(clip_str)
+ except ValueError:
+ print(f"{error('XY Plot Error:')} The LoRA model strength and clip strength values should be numbers"
+ f" between {bounds['LoRA']['model_str']['min']} and {bounds['LoRA']['model_str']['max']}.")
+ return None
+
+ if model_str < bounds["LoRA"]["model_str"]["min"] or model_str > bounds["LoRA"]["model_str"]["max"]:
+ print(f"{error('XY Plot Error:')} '{model_str}' is not a valid LoRA model strength value. "
+ f"Valid lora model strength values are between {bounds['LoRA']['model_str']['min']} and {bounds['LoRA']['model_str']['max']}.")
+ model_str = None
+
+ if clip_str < bounds["LoRA"]["clip_str"]["min"] or clip_str > bounds["LoRA"]["clip_str"]["max"]:
+ print(f"{error('XY Plot Error:')} '{clip_str}' is not a valid LoRA clip strength value. "
+ f"Valid lora clip strength values are between {bounds['LoRA']['clip_str']['min']} and {bounds['LoRA']['clip_str']['max']}.")
+ clip_str = None
+
+ if lora_name is None or model_str is None or clip_str is None:
+ return None
+ else:
+ return lora_name, model_str, clip_str
+ else:
+ if value not in bounds["LoRA"]["options"]:
+ valid_loras = '\n'.join(bounds["LoRA"]["options"])
+ print(f"{error('XY Plot Error:')} '{value}' is not a valid LoRA. Valid LoRAs are:\n{valid_loras}")
+ return None
+ else:
+ return value, 1.0, 1.0
+
+ # __________________________________________________________________________________________________________
+ else:
+ return None
+
+ # Validate plot_value array length is 1 if doing a "Seeds++ Batch"
+ if len(plot_value) != 1 and plot_type == "Seeds++ Batch":
+ print(f"{error('XY Plot Error:')} '{';'.join(plot_value)}' is not a valid batch count.")
+ return (None,)
+
+ # Apply allowed shortcut syntax to certain input types
+ if plot_type in ["Sampler", "Checkpoint", "LoRA"]:
+ if plot_value[-1].startswith(','):
+ # Remove the leading comma from the last entry and store it as suffixes
+ suffixes = plot_value.pop().lstrip(',').split(',')
+ # Split all preceding entries into subentries
+ plot_value = [entry.split(',') for entry in plot_value]
+ # Make all entries the same length as suffixes by appending missing elements
+ for entry in plot_value:
+ entry += suffixes[len(entry) - 1:]
+ # Join subentries back into strings
+ plot_value = [','.join(entry) for entry in plot_value]
+
+ # Prompt S/R X Cleanup
+ if plot_type in {"Positive Prompt S/R", "Negative Prompt S/R"}:
+ if plot_value[0] == '':
+ print(f"{error('XY Plot Error:')} Prompt S/R value can not be empty.")
+ return (None,)
+ else:
+ plot_value = [(plot_value[0], None) if i == 0 else (plot_value[0], x) for i, x in enumerate(plot_value)]
+
+ # Loop over each entry in plot_value and check if it's valid
+ if plot_type not in {"Nothing", "Positive Prompt S/R", "Negative Prompt S/R"}:
+ for i in range(len(plot_value)):
+ plot_value[i] = validate_value(plot_value[i], plot_type, bounds)
+ if plot_value[i] == None:
+ return (None,)
+
+ # Set up Seeds++ Batch structure
+ if plot_type == "Seeds++ Batch":
+ plot_value = list(range(plot_value[0]))
+
+ # Nest LoRA value in another array to reflect LoRA stack changes
+ if plot_type == "LoRA":
+ plot_value = [[x] for x in plot_value]
+
+ # Clean X/Y_values
+ if plot_type == "Nothing":
+ plot_value = [""]
+
+ return ((plot_type, plot_value),)
+
+#=======================================================================================================================
+# TSC XY Plot: Join Inputs
+class TSC_XYplot_JoinInputs:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "XY_1": ("XY",),
+ "XY_2": ("XY",),},
+ }
+
+ RETURN_TYPES = ("XY",)
+ RETURN_NAMES = ("X or Y",)
+ FUNCTION = "xy_value"
+ CATEGORY = "Efficiency Nodes/XY Inputs"
+
+ def xy_value(self, XY_1, XY_2):
+ xy_type_1, xy_value_1 = XY_1
+ xy_type_2, xy_value_2 = XY_2
+
+ if xy_type_1 != xy_type_2:
+ print(f"{error('Join XY Inputs Error:')} Input types must match")
+ return (None,)
+ elif xy_type_1 == "Seeds++ Batch":
+ xy_type = xy_type_1
+ combined_length = len(xy_value_1) + len(xy_value_2)
+ xy_value = list(range(combined_length))
+ elif xy_type_1 == "Positive Prompt S/R" or xy_type_1 == "Negative Prompt S/R":
+ xy_type = xy_type_1
+ xy_value = xy_value_1 + [(xy_value_1[0][0], t[1]) for t in xy_value_2[1:]]
+ else:
+ xy_type = xy_type_1
+ xy_value = xy_value_1 + xy_value_2
+ return ((xy_type, xy_value),)
+
+########################################################################################################################
+# TSC Image Overlay
+class TSC_ImageOverlay:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "base_image": ("IMAGE",),
+ "overlay_image": ("IMAGE",),
+ "overlay_resize": (["None", "Fit", "Resize by rescale_factor", "Resize to width & heigth"],),
+ "resize_method": (["nearest-exact", "bilinear", "area"],),
+ "rescale_factor": ("FLOAT", {"default": 1, "min": 0.01, "max": 16.0, "step": 0.1}),
+ "width": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 64}),
+ "height": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 64}),
+ "x_offset": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 10}),
+ "y_offset": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 10}),
+ "rotation": ("INT", {"default": 0, "min": -180, "max": 180, "step": 5}),
+ "opacity": ("FLOAT", {"default": 0, "min": 0, "max": 100, "step": 5}),
+ },
+ "optional": {"optional_mask": ("MASK",),}
+ }
+
+ RETURN_TYPES = ("IMAGE",)
+ FUNCTION = "apply_overlay_image"
+ CATEGORY = "Efficiency Nodes/Image"
+
+ def apply_overlay_image(self, base_image, overlay_image, overlay_resize, resize_method, rescale_factor,
+ width, height, x_offset, y_offset, rotation, opacity, optional_mask=None):
+
+ # Pack tuples and assign variables
+ size = width, height
+ location = x_offset, y_offset
+ mask = optional_mask
+
+ # Check for different sizing options
+ if overlay_resize != "None":
+ #Extract overlay_image size and store in Tuple "overlay_image_size" (WxH)
+ overlay_image_size = overlay_image.size()
+ overlay_image_size = (overlay_image_size[2], overlay_image_size[1])
+ if overlay_resize == "Fit":
+ h_ratio = base_image.size()[1] / overlay_image_size[1]
+ w_ratio = base_image.size()[2] / overlay_image_size[0]
+ ratio = min(h_ratio, w_ratio)
+ overlay_image_size = tuple(round(dimension * ratio) for dimension in overlay_image_size)
+ elif overlay_resize == "Resize by rescale_factor":
+ overlay_image_size = tuple(int(dimension * rescale_factor) for dimension in overlay_image_size)
+ elif overlay_resize == "Resize to width & heigth":
+ overlay_image_size = (size[0], size[1])
+
+ samples = overlay_image.movedim(-1, 1)
+ overlay_image = comfy.utils.common_upscale(samples, overlay_image_size[0], overlay_image_size[1], resize_method, False)
+ overlay_image = overlay_image.movedim(1, -1)
+
+ overlay_image = tensor2pil(overlay_image)
+
+ # Add Alpha channel to overlay
+ overlay_image = overlay_image.convert('RGBA')
+ overlay_image.putalpha(Image.new("L", overlay_image.size, 255))
+
+ # If mask connected, check if the overlay_image image has an alpha channel
+ if mask is not None:
+ # Convert mask to pil and resize
+ mask = tensor2pil(mask)
+ mask = mask.resize(overlay_image.size)
+ # Apply mask as overlay's alpha
+ overlay_image.putalpha(ImageOps.invert(mask))
+
+ # Rotate the overlay image
+ overlay_image = overlay_image.rotate(rotation, expand=True)
+
+ # Apply opacity on overlay image
+ r, g, b, a = overlay_image.split()
+ a = a.point(lambda x: max(0, int(x * (1 - opacity / 100))))
+ overlay_image.putalpha(a)
+
+ # Split the base_image tensor along the first dimension to get a list of tensors
+ base_image_list = torch.unbind(base_image, dim=0)
+
+ # Convert each tensor to a PIL image, apply the overlay, and then convert it back to a tensor
+ processed_base_image_list = []
+ for tensor in base_image_list:
+ # Convert tensor to PIL Image
+ image = tensor2pil(tensor)
+
+ # Paste the overlay image onto the base image
+ if mask is None:
+ image.paste(overlay_image, location)
+ else:
+ image.paste(overlay_image, location, overlay_image)
+
+ # Convert PIL Image back to tensor
+ processed_tensor = pil2tensor(image)
+
+ # Append to list
+ processed_base_image_list.append(processed_tensor)
+
+ # Combine the processed images back into a single tensor
+ base_image = torch.stack([tensor.squeeze() for tensor in processed_base_image_list])
+
+ # Return the edited base image
+ return (base_image,)
+
+########################################################################################################################
+# Noise Sources & Seed Variations
+# https://github.com/shiimizu/ComfyUI_smZNodes
+# https://github.com/chrisgoringe/cg-noise
+
+# TSC Noise Sources & Variations Script
+class TSC_Noise_Control_Script:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "rng_source": (["cpu", "gpu", "nv"],),
+ "cfg_denoiser": ("BOOLEAN", {"default": False}),
+ "add_seed_noise": ("BOOLEAN", {"default": False}),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "weight": ("FLOAT", {"default": 0.015, "min": 0, "max": 1, "step": 0.001})},
+ "optional": {"script": ("SCRIPT",)}
+ }
+
+ RETURN_TYPES = ("SCRIPT",)
+ RETURN_NAMES = ("SCRIPT",)
+ FUNCTION = "noise_control"
+ CATEGORY = "Efficiency Nodes/Scripts"
+
+ def noise_control(self, rng_source, cfg_denoiser, add_seed_noise, seed, weight, script=None):
+ script = script or {}
+ script["noise"] = (rng_source, cfg_denoiser, add_seed_noise, seed, weight)
+ return (script,)
+
+########################################################################################################################
+# Add controlnet options if have controlnet_aux installed (https://github.com/Fannovel16/comfyui_controlnet_aux)
+use_controlnet_widget = preprocessor_widget = (["_"],)
+if os.path.exists(os.path.join(custom_nodes_dir, "comfyui_controlnet_aux")):
+ printout = "Attempting to add Control Net options to the 'HiRes-Fix Script' Node (comfyui_controlnet_aux add-on)..."
+ #print(f"{message('Efficiency Nodes:')} {printout}", end="", flush=True)
+
+ try:
+ with suppress_output():
+ AIO_Preprocessor = getattr(import_module("comfyui_controlnet_aux.__init__"), 'AIO_Preprocessor')
+ use_controlnet_widget = ("BOOLEAN", {"default": False})
+ preprocessor_widget = AIO_Preprocessor.INPUT_TYPES()["optional"]["preprocessor"]
+ print(f"\r{message('Efficiency Nodes:')} {printout}{success('Success!')}")
+ except Exception:
+ print(f"\r{message('Efficiency Nodes:')} {printout}{error('Failed!')}")
+
+# TSC HighRes-Fix with model latent upscalers (https://github.com/city96/SD-Latent-Upscaler)
+class TSC_HighRes_Fix:
+
+ default_latent_upscalers = LatentUpscaleBy.INPUT_TYPES()["required"]["upscale_method"][0]
+
+ city96_upscale_methods =\
+ ["city96." + ver for ver in city96_latent_upscaler.LatentUpscaler.INPUT_TYPES()["required"]["latent_ver"][0]]
+ city96_scalings_raw = city96_latent_upscaler.LatentUpscaler.INPUT_TYPES()["required"]["scale_factor"][0]
+ city96_scalings_float = [float(scale) for scale in city96_scalings_raw]
+
+ ttl_nn_upscale_methods = \
+ ["ttl_nn." + ver for ver in
+ ttl_nn_latent_upscaler.NNLatentUpscale.INPUT_TYPES()["required"]["version"][0]]
+
+ latent_upscalers = default_latent_upscalers + city96_upscale_methods + ttl_nn_upscale_methods
+ pixel_upscalers = folder_paths.get_filename_list("upscale_models")
+
+ @classmethod
+ def INPUT_TYPES(cls):
+
+ return {"required": {"upscale_type": (["latent","pixel"],),
+ "hires_ckpt_name": (["(use same)"] + folder_paths.get_filename_list("checkpoints"),),
+ "latent_upscaler": (cls.latent_upscalers,),
+ "pixel_upscaler": (cls.pixel_upscalers,),
+ "upscale_by": ("FLOAT", {"default": 1.25, "min": 0.01, "max": 8.0, "step": 0.05}),
+ "use_same_seed": ("BOOLEAN", {"default": True}),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "hires_steps": ("INT", {"default": 12, "min": 1, "max": 10000}),
+ "denoise": ("FLOAT", {"default": .56, "min": 0.00, "max": 1.00, "step": 0.01}),
+ "iterations": ("INT", {"default": 1, "min": 0, "max": 5, "step": 1}),
+ "use_controlnet": use_controlnet_widget,
+ "control_net_name": (folder_paths.get_filename_list("controlnet"),),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
+ "preprocessor": preprocessor_widget,
+ "preprocessor_imgs": ("BOOLEAN", {"default": False})
+ },
+ "optional": {"script": ("SCRIPT",)},
+ "hidden": {"my_unique_id": "UNIQUE_ID"}
+ }
+
+ RETURN_TYPES = ("SCRIPT",)
+ FUNCTION = "hires_fix_script"
+ CATEGORY = "Efficiency Nodes/Scripts"
+
+ def hires_fix_script(self, upscale_type, hires_ckpt_name, latent_upscaler, pixel_upscaler, upscale_by,
+ use_same_seed, seed, hires_steps, denoise, iterations, use_controlnet, control_net_name,
+ strength, preprocessor, preprocessor_imgs, script=None, my_unique_id=None):
+ latent_upscale_function = None
+ latent_upscale_model = None
+ pixel_upscale_model = None
+
+ def float_to_string(num):
+ if num == int(num):
+ return "{:.1f}".format(num)
+ else:
+ return str(num)
+
+ if iterations > 0 and upscale_by > 0:
+ if upscale_type == "latent":
+ # For latent methods from city96
+ if latent_upscaler in self.city96_upscale_methods:
+ # Remove extra characters added
+ latent_upscaler = latent_upscaler.replace("city96.", "")
+
+ # Set function to city96_latent_upscaler.LatentUpscaler
+ latent_upscale_function = city96_latent_upscaler.LatentUpscaler
+
+ # Find the nearest valid scaling in city96_scalings_float
+ nearest_scaling = min(self.city96_scalings_float, key=lambda x: abs(x - upscale_by))
+
+ # Retrieve the index of the nearest scaling
+ nearest_scaling_index = self.city96_scalings_float.index(nearest_scaling)
+
+ # Use the index to get the raw string representation
+ nearest_scaling_raw = self.city96_scalings_raw[nearest_scaling_index]
+
+ upscale_by = float_to_string(upscale_by)
+
+ # Check if the input upscale_by value was different from the nearest valid value
+ if upscale_by != nearest_scaling_raw:
+ print(f"{warning('HighRes-Fix Warning:')} "
+ f"When using 'city96.{latent_upscaler}', 'upscale_by' must be one of {self.city96_scalings_raw}.\n"
+ f"Rounding to the nearest valid value ({nearest_scaling_raw}).\033[0m")
+ upscale_by = nearest_scaling_raw
+
+ # For ttl upscale methods
+ elif latent_upscaler in self.ttl_nn_upscale_methods:
+ # Remove extra characters added
+ latent_upscaler = latent_upscaler.replace("ttl_nn.", "")
+
+ # Bound to min/max limits
+ upscale_by_clamped = min(max(upscale_by, 1), 2)
+ if upscale_by != upscale_by_clamped:
+ print(f"{warning('HighRes-Fix Warning:')} "
+ f"When using 'ttl_nn.{latent_upscaler}', 'upscale_by' must be between 1 and 2.\n"
+ f"Rounding to the nearest valid value ({upscale_by_clamped}).\033[0m")
+ upscale_by = upscale_by_clamped
+
+ latent_upscale_function = ttl_nn_latent_upscaler.NNLatentUpscale
+
+ # For default upscale methods
+ elif latent_upscaler in self.default_latent_upscalers:
+ latent_upscale_function = LatentUpscaleBy
+
+ else: # Default
+ latent_upscale_function = LatentUpscaleBy
+ latent_upscaler = self.default_latent_upscalers[0]
+ print(f"{warning('HiResFix Script Warning:')} Chosen latent upscale method not found! "
+ f"defaulting to '{latent_upscaler}'.\n")
+
+ # Load Checkpoint if defined
+ if hires_ckpt_name == "(use same)":
+ clear_cache(my_unique_id, 0, "ckpt")
+ else:
+ latent_upscale_model, _, _ = \
+ load_checkpoint(hires_ckpt_name, my_unique_id, output_vae=False, cache=1, cache_overwrite=True)
+
+ elif upscale_type == "pixel":
+ pixel_upscale_model = UpscaleModelLoader().load_model(pixel_upscaler)[0]
+
+ control_net = ControlNetLoader().load_controlnet(control_net_name)[0] if use_controlnet is True else None
+
+ # Construct the script output
+ script = script or {}
+ script["hiresfix"] = (upscale_type, latent_upscaler, upscale_by, use_same_seed, seed, hires_steps,
+ denoise, iterations, control_net, strength, preprocessor, preprocessor_imgs,
+ latent_upscale_function, latent_upscale_model, pixel_upscale_model)
+
+ return (script,)
+
+########################################################################################################################
+# TSC Tiled Upscaler (https://github.com/BlenderNeko/ComfyUI_TiledKSampler)
+class TSC_Tiled_Upscaler:
+ @classmethod
+ def INPUT_TYPES(cls):
+ # Split the list based on the keyword "tile"
+ cnet_filenames = [name for name in folder_paths.get_filename_list("controlnet")]
+
+ return {"required": {"upscale_by": ("FLOAT", {"default": 1.25, "min": 0.01, "max": 8.0, "step": 0.05}),
+ "tile_size": ("INT", {"default": 512, "min": 256, "max": MAX_RESOLUTION, "step": 64}),
+ "tiling_strategy": (["random", "random strict", "padded", 'simple', 'none'],),
+ "tiling_steps": ("INT", {"default": 30, "min": 1, "max": 10000}),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "denoise": ("FLOAT", {"default": .4, "min": 0.0, "max": 1.0, "step": 0.01}),
+ "use_controlnet": ("BOOLEAN", {"default": False}),
+ "tile_controlnet": (cnet_filenames,),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
+ },
+ "optional": {"script": ("SCRIPT",)}}
+
+ RETURN_TYPES = ("SCRIPT",)
+ FUNCTION = "tiled_sampling"
+ CATEGORY = "Efficiency Nodes/Scripts"
+
+ def tiled_sampling(self, upscale_by, tile_size, tiling_strategy, tiling_steps, seed, denoise,
+ use_controlnet, tile_controlnet, strength, script=None):
+ if tiling_strategy != 'none':
+ script = script or {}
+ tile_controlnet = ControlNetLoader().load_controlnet(tile_controlnet)[0] if use_controlnet else None
+
+ script["tile"] = (upscale_by, tile_size, tiling_strategy, tiling_steps, seed, denoise, tile_controlnet, strength)
+ return (script,)
+
+########################################################################################################################
+# NODE MAPPING
+NODE_CLASS_MAPPINGS = {
+ "KSampler (Efficient)": TSC_KSampler,
+ "KSampler Adv. (Efficient)":TSC_KSamplerAdvanced,
+ "KSampler SDXL (Eff.)": TSC_KSamplerSDXL,
+ "Efficient Loader": TSC_EfficientLoader,
+ "Eff. Loader SDXL": TSC_EfficientLoaderSDXL,
+ "LoRA Stacker": TSC_LoRA_Stacker,
+ "Control Net Stacker": TSC_Control_Net_Stacker,
+ "Apply ControlNet Stack": TSC_Apply_ControlNet_Stack,
+ "Unpack SDXL Tuple": TSC_Unpack_SDXL_Tuple,
+ "Pack SDXL Tuple": TSC_Pack_SDXL_Tuple,
+ "XY Plot": TSC_XYplot,
+ "XY Input: Seeds++ Batch": TSC_XYplot_SeedsBatch,
+ "XY Input: Add/Return Noise": TSC_XYplot_AddReturnNoise,
+ "XY Input: Steps": TSC_XYplot_Steps,
+ "XY Input: CFG Scale": TSC_XYplot_CFG,
+ "XY Input: Sampler/Scheduler": TSC_XYplot_Sampler_Scheduler,
+ "XY Input: Denoise": TSC_XYplot_Denoise,
+ "XY Input: VAE": TSC_XYplot_VAE,
+ "XY Input: Prompt S/R": TSC_XYplot_PromptSR,
+ "XY Input: Aesthetic Score": TSC_XYplot_AScore,
+ "XY Input: Refiner On/Off": TSC_XYplot_Refiner_OnOff,
+ "XY Input: Checkpoint": TSC_XYplot_Checkpoint,
+ "XY Input: Clip Skip": TSC_XYplot_ClipSkip,
+ "XY Input: LoRA": TSC_XYplot_LoRA,
+ "XY Input: LoRA Plot": TSC_XYplot_LoRA_Plot,
+ "XY Input: LoRA Stacks": TSC_XYplot_LoRA_Stacks,
+ "XY Input: Control Net": TSC_XYplot_Control_Net,
+ "XY Input: Control Net Plot": TSC_XYplot_Control_Net_Plot,
+ "XY Input: Manual XY Entry": TSC_XYplot_Manual_XY_Entry,
+ "Manual XY Entry Info": TSC_XYplot_Manual_XY_Entry_Info,
+ "Join XY Inputs of Same Type": TSC_XYplot_JoinInputs,
+ "Image Overlay": TSC_ImageOverlay,
+ "Noise Control Script": TSC_Noise_Control_Script,
+ "HighRes-Fix Script": TSC_HighRes_Fix,
+ "Tiled Upscaler Script": TSC_Tiled_Upscaler
+}
+
+########################################################################################################################
+# Add AnimateDiff Script based off Kosinkadink's Nodes (https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved)
+if os.path.exists(os.path.join(custom_nodes_dir, "ComfyUI-AnimateDiff-Evolved")):
+ printout = "Attempting to add 'AnimatedDiff Script' Node (ComfyUI-AnimateDiff-Evolved add-on)..."
+ print(f"{message('Efficiency Nodes:')} {printout}", end="")
+ try:
+ module = import_module("ComfyUI-AnimateDiff-Evolved.animatediff.nodes")
+ AnimateDiffLoaderWithContext = getattr(module, 'AnimateDiffLoaderWithContext')
+ AnimateDiffCombine = getattr(module, 'AnimateDiffCombine_Deprecated')
+ print(f"\r{message('Efficiency Nodes:')} {printout}{success('Success!')}")
+
+ # TSC AnimatedDiff Script (https://github.com/BlenderNeko/ComfyUI_TiledKSampler)
+ class TSC_AnimateDiff_Script:
+ @classmethod
+ def INPUT_TYPES(cls):
+
+ return {"required": {
+ "motion_model": AnimateDiffLoaderWithContext.INPUT_TYPES()["required"]["model_name"],
+ "beta_schedule": AnimateDiffLoaderWithContext.INPUT_TYPES()["required"]["beta_schedule"],
+ "frame_rate": AnimateDiffCombine.INPUT_TYPES()["required"]["frame_rate"],
+ "loop_count": AnimateDiffCombine.INPUT_TYPES()["required"]["loop_count"],
+ "format": AnimateDiffCombine.INPUT_TYPES()["required"]["format"],
+ "pingpong": AnimateDiffCombine.INPUT_TYPES()["required"]["pingpong"],
+ "save_image": AnimateDiffCombine.INPUT_TYPES()["required"]["save_image"]},
+ "optional": {"context_options": ("CONTEXT_OPTIONS",)}
+ }
+
+ RETURN_TYPES = ("SCRIPT",)
+ FUNCTION = "animatediff"
+ CATEGORY = "Efficiency Nodes/Scripts"
+
+ def animatediff(self, motion_model, beta_schedule, frame_rate, loop_count, format, pingpong, save_image,
+ script=None, context_options=None):
+ script = script or {}
+ script["anim"] = (motion_model, beta_schedule, context_options, frame_rate, loop_count, format, pingpong, save_image)
+ return (script,)
+
+ NODE_CLASS_MAPPINGS.update({"AnimateDiff Script": TSC_AnimateDiff_Script})
+
+ except Exception:
+ print(f"\r{message('Efficiency Nodes:')} {printout}{error('Failed!')}")
+
+########################################################################################################################
+# Simpleeval Nodes (https://github.com/danthedeckie/simpleeval)
+try:
+ import simpleeval
+
+ # TSC Evaluate Integers
+ class TSC_EvaluateInts:
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "python_expression": ("STRING", {"default": "((a + b) - c) / 2", "multiline": False}),
+ "print_to_console": (["False", "True"],), },
+ "optional": {
+ "a": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}),
+ "b": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}),
+ "c": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}), },
+ }
+
+ RETURN_TYPES = ("INT", "FLOAT", "STRING",)
+ OUTPUT_NODE = True
+ FUNCTION = "evaluate"
+ CATEGORY = "Efficiency Nodes/Simple Eval"
+
+ def evaluate(self, python_expression, print_to_console, a=0, b=0, c=0):
+ # simple_eval doesn't require the result to be converted to a string
+ result = simpleeval.simple_eval(python_expression, names={'a': a, 'b': b, 'c': c})
+ int_result = int(result)
+ float_result = float(result)
+ string_result = str(result)
+ if print_to_console == "True":
+ print(f"\n{error('Evaluate Integers:')}")
+ print(f"\033[90m{{a = {a} , b = {b} , c = {c}}} \033[0m")
+ print(f"{python_expression} = \033[92m INT: " + str(int_result) + " , FLOAT: " + str(
+ float_result) + ", STRING: " + string_result + "\033[0m")
+ return (int_result, float_result, string_result,)
+
+
+ # ==================================================================================================================
+ # TSC Evaluate Floats
+ class TSC_EvaluateFloats:
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "python_expression": ("STRING", {"default": "((a + b) - c) / 2", "multiline": False}),
+ "print_to_console": (["False", "True"],), },
+ "optional": {
+ "a": ("FLOAT", {"default": 0, "min": -sys.float_info.max, "max": sys.float_info.max, "step": 1}),
+ "b": ("FLOAT", {"default": 0, "min": -sys.float_info.max, "max": sys.float_info.max, "step": 1}),
+ "c": ("FLOAT", {"default": 0, "min": -sys.float_info.max, "max": sys.float_info.max, "step": 1}), },
+ }
+
+ RETURN_TYPES = ("INT", "FLOAT", "STRING",)
+ OUTPUT_NODE = True
+ FUNCTION = "evaluate"
+ CATEGORY = "Efficiency Nodes/Simple Eval"
+
+ def evaluate(self, python_expression, print_to_console, a=0, b=0, c=0):
+ # simple_eval doesn't require the result to be converted to a string
+ result = simpleeval.simple_eval(python_expression, names={'a': a, 'b': b, 'c': c})
+ int_result = int(result)
+ float_result = float(result)
+ string_result = str(result)
+ if print_to_console == "True":
+ print(f"\n{error('Evaluate Floats:')}")
+ print(f"\033[90m{{a = {a} , b = {b} , c = {c}}} \033[0m")
+ print(f"{python_expression} = \033[92m INT: " + str(int_result) + " , FLOAT: " + str(
+ float_result) + ", STRING: " + string_result + "\033[0m")
+ return (int_result, float_result, string_result,)
+
+
+ # ==================================================================================================================
+ # TSC Evaluate Strings
+ class TSC_EvaluateStrs:
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {"required": {
+ "python_expression": ("STRING", {"default": "a + b + c", "multiline": False}),
+ "print_to_console": (["False", "True"],)},
+ "optional": {
+ "a": ("STRING", {"default": "Hello", "multiline": False}),
+ "b": ("STRING", {"default": " World", "multiline": False}),
+ "c": ("STRING", {"default": "!", "multiline": False}), }
+ }
+
+ RETURN_TYPES = ("STRING",)
+ OUTPUT_NODE = True
+ FUNCTION = "evaluate"
+ CATEGORY = "Efficiency Nodes/Simple Eval"
+
+ def evaluate(self, python_expression, print_to_console, a="", b="", c=""):
+ variables = {'a': a, 'b': b, 'c': c} # Define the variables for the expression
+
+ functions = simpleeval.DEFAULT_FUNCTIONS.copy()
+ functions.update({"len": len}) # Add the functions for the expression
+
+ result = simpleeval.simple_eval(python_expression, names=variables, functions=functions)
+ if print_to_console == "True":
+ print(f"\n{error('Evaluate Strings:')}")
+ print(f"\033[90ma = {a} \nb = {b} \nc = {c}\033[0m")
+ print(f"{python_expression} = \033[92m" + str(result) + "\033[0m")
+ return (str(result),) # Convert result to a string before returning
+
+
+ # ==================================================================================================================
+ # TSC Simple Eval Examples
+ class TSC_EvalExamples:
+ @classmethod
+ def INPUT_TYPES(cls):
+ filepath = os.path.join(my_dir, 'workflows', 'SimpleEval_Node_Examples.txt')
+ with open(filepath, 'r') as file:
+ examples = file.read()
+ return {"required": {"models_text": ("STRING", {"default": examples, "multiline": True}), }, }
+
+ RETURN_TYPES = ()
+ CATEGORY = "Efficiency Nodes/Simple Eval"
+
+ # ==================================================================================================================
+ NODE_CLASS_MAPPINGS.update({"Evaluate Integers": TSC_EvaluateInts})
+ NODE_CLASS_MAPPINGS.update({"Evaluate Floats": TSC_EvaluateFloats})
+ NODE_CLASS_MAPPINGS.update({"Evaluate Strings": TSC_EvaluateStrs})
+ NODE_CLASS_MAPPINGS.update({"Simple Eval Examples": TSC_EvalExamples})
+
+except ImportError:
+ print(f"{warning('Efficiency Nodes Warning:')} Failed to import python package 'simpleeval'; related nodes disabled.\n")
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/funding.yml b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/funding.yml
new file mode 100644
index 0000000000000000000000000000000000000000..648a4a8da4c39b70d4775182e4548a914f44b06d
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/funding.yml
@@ -0,0 +1,3 @@
+github: ["https://www.buymeacoffee.com/jagsai", jags111]
+patreon: jags111
+custom: ["https://www.paypal.me/revsmart", orchidsasia.com]
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-10-31_22-43-17.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-10-31_22-43-17.png
new file mode 100644
index 0000000000000000000000000000000000000000..1b8032bdf0c4a9894db3efcea9dd9dc30c600294
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-10-31_22-43-17.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b5a7fc60af089c2cf16a0c6e279e11c2c784e44d9bf3b07fa21a3f6743976862
+size 1219515
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-01_14-25-01.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-01_14-25-01.png
new file mode 100644
index 0000000000000000000000000000000000000000..0b44ceb0b69992aac4e0fe3ae3a100c9e154e69a
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-01_14-25-01.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-01_19-54-59.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-01_19-54-59.png
new file mode 100644
index 0000000000000000000000000000000000000000..d9f8d41bb37f70aa47c3e3af9e47838ea20d3930
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-01_19-54-59.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-32-57.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-32-57.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3a17b4c0494a34268dea04ce4ab96dc76c061de
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-32-57.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3bc213511bc8fb1114ba5cfbbef35a8421f6bda42cc50c6796128a276c2f0016
+size 1655600
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-33-57.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-33-57.png
new file mode 100644
index 0000000000000000000000000000000000000000..859fd0e35d8d70b773d6d0df25d908c32d0c802c
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-33-57.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-46-20.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-46-20.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec8dbc08ebb18544f215c0c180edeeb6f735c26f
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-46-20.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:418fd34689a838eef9a31690fd7bbf636e5b2a2e3177d622670c74c75f80bab5
+size 1979121
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-47-09.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-47-09.png
new file mode 100644
index 0000000000000000000000000000000000000000..6275b1afb80c60c60fad017159e96ba2f448a55a
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-47-09.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:87fcbac500ba9a65ba59738ac82cf4d7c228902f9115ec380a49cbab132e8fb4
+size 1325680
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-57-28.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-57-28.png
new file mode 100644
index 0000000000000000000000000000000000000000..1982e3de24268f49e1603a2f53b12e2231ddade3
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_22-57-28.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2899271284256a2cc516bce60757a12f84400c42302c6ecbda45a15edf07dc5c
+size 1018241
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_23-10-03.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_23-10-03.png
new file mode 100644
index 0000000000000000000000000000000000000000..46a6f679c2b706d22d8fda7170e35b62851732be
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-04_23-10-03.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-05_00-02-07.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-05_00-02-07.png
new file mode 100644
index 0000000000000000000000000000000000000000..464aed87fe31256d2756a5acb87b7fc8d07f3067
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-05_00-02-07.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-05_00-02-34.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-05_00-02-34.png
new file mode 100644
index 0000000000000000000000000000000000000000..03a07345368350107e4a6bd3b284aa8fb975b11c
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-11-05_00-02-34.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:82ab19aa487831aa3ae55012c8e0fc9fbf6c1333c605e82dd6c81213d6e19d48
+size 1178567
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-12-08_19-53-37.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-12-08_19-53-37.png
new file mode 100644
index 0000000000000000000000000000000000000000..ebc3bbd3c0592b8ed4a7da7bbe9b0053cd5e9a38
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-12-08_19-53-37.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-12-08_19-54-11.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-12-08_19-54-11.png
new file mode 100644
index 0000000000000000000000000000000000000000..120b16356db37644db2408fb8b9725b59f463369
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/2023-12-08_19-54-11.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/ComfyUI_temp_vpose_00005_.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/ComfyUI_temp_vpose_00005_.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff9db61e9275eb8a53bf18eee726a0ac2b5c8a8d
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/ComfyUI_temp_vpose_00005_.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84661dabb42054b900dea3c9f3b63c3667d9fb432c51ea4f3b20dbb680c29d9f
+size 11000334
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/README.md b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..850877c58f0c157e66feb0fa90e3e0ccbda2f212
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/README.md
@@ -0,0 +1,42 @@
+Images in this group
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/AnimateDiff & HiResFix Scripts.gif b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/AnimateDiff & HiResFix Scripts.gif
new file mode 100644
index 0000000000000000000000000000000000000000..4662c6f50cef4fb578ee4c036a101baa5dc9f7ba
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/AnimateDiff & HiResFix Scripts.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d4332dda6b5a323359de760cb57930ad9ca613eb65632e85e93a938be14e57c8
+size 6631509
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/AnimateDiff - Node Example.gif b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/AnimateDiff - Node Example.gif
new file mode 100644
index 0000000000000000000000000000000000000000..b44b1f62c18ae60f89ddb61dac853eeb08b9b3e9
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/AnimateDiff - Node Example.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fa88ea5d256f8f4e2dfed6aa387257a6b434ca81540af2fe64370a75104872fb
+size 4283195
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/HighResFix - Node Example.gif b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/HighResFix - Node Example.gif
new file mode 100644
index 0000000000000000000000000000000000000000..d067723014053995aebed525401e37bb50174c5e
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/HighResFix - Node Example.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:339559d35e18ad532d5b255ea268fca6eba2eb046c215c588520b4bf3293112d
+size 16751370
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/Image Overlay - Node Example.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/Image Overlay - Node Example.png
new file mode 100644
index 0000000000000000000000000000000000000000..c046b604f54cabe41b2379a683fbff851486096d
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/Image Overlay - Node Example.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ad1684e6413a1b5435169b8abb388a4c0e39014b6af74bc3e8d6c216bc9b0802
+size 1329617
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - AnimateDiff Script.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - AnimateDiff Script.png
new file mode 100644
index 0000000000000000000000000000000000000000..9493b9af432ddcb4f2dad9e6515cd37e8ddd4353
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - AnimateDiff Script.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Eff. Loader SDXL.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Eff. Loader SDXL.png
new file mode 100644
index 0000000000000000000000000000000000000000..8e32cee703c4b0606d3a243f199bd399662de0b3
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Eff. Loader SDXL.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Efficient Loader.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Efficient Loader.png
new file mode 100644
index 0000000000000000000000000000000000000000..1507c172a151fd00c28a5cbcb51d103af6aa1f1c
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Efficient Loader.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Evaluate Floats.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Evaluate Floats.png
new file mode 100644
index 0000000000000000000000000000000000000000..d667bbe3184ea2172e985b1d990ab91c17d80ad2
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Evaluate Floats.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Evaluate Integers.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Evaluate Integers.png
new file mode 100644
index 0000000000000000000000000000000000000000..7f6df611d44e8518e7d447fe5aa21340213b414f
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Evaluate Integers.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Evaluate Strings.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Evaluate Strings.png
new file mode 100644
index 0000000000000000000000000000000000000000..49081b2de63aec44bec46fd2ffdd44b251b965d6
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Evaluate Strings.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - HighRes-Fix Script.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - HighRes-Fix Script.png
new file mode 100644
index 0000000000000000000000000000000000000000..f123f17974a694a552d6afe9968deff9cacd4f5a
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - HighRes-Fix Script.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Image Overlay.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Image Overlay.png
new file mode 100644
index 0000000000000000000000000000000000000000..b50802ac373420e7e6e9108a5646759ef8eb654d
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Image Overlay.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - KSampler (Efficient).png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - KSampler (Efficient).png
new file mode 100644
index 0000000000000000000000000000000000000000..6b155e0e152d7fdcb61f6d0c67de304561cde882
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - KSampler (Efficient).png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - KSampler Adv. (Efficient).png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - KSampler Adv. (Efficient).png
new file mode 100644
index 0000000000000000000000000000000000000000..42923b895cae81b3d27657fdaddaa0606b7edda7
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - KSampler Adv. (Efficient).png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - KSampler SDXL (Eff.).png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - KSampler SDXL (Eff.).png
new file mode 100644
index 0000000000000000000000000000000000000000..53ae16a25cef07948b24528aa27a8004cccc9b5b
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - KSampler SDXL (Eff.).png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Noise Control Script.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Noise Control Script.png
new file mode 100644
index 0000000000000000000000000000000000000000..1fa33ae69cdcc2a9d3597139a26e6a9ddf765ec7
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Noise Control Script.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Tiled Upscaler Script.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Tiled Upscaler Script.png
new file mode 100644
index 0000000000000000000000000000000000000000..b8c5d3513d6b27cb8d581d912d031c591863980a
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - Tiled Upscaler Script.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - XY Plot Script.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - XY Plot Script.png
new file mode 100644
index 0000000000000000000000000000000000000000..87292c603ba4c36abff5565e2facc4e205fa4b89
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - XY Plot Script.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - XY Plot.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - XY Plot.png
new file mode 100644
index 0000000000000000000000000000000000000000..98c1d7693804d623640fcd4dd271ee489c25876a
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NODE - XY Plot.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NodeMenu - Efficient Loaders.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NodeMenu - Efficient Loaders.png
new file mode 100644
index 0000000000000000000000000000000000000000..03142924d87145255da682776543595d9136b88b
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/NodeMenu - Efficient Loaders.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/ScriptChain.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/ScriptChain.png
new file mode 100644
index 0000000000000000000000000000000000000000..fbf6d33782711a1a9c8e29fbf9ba4b5c6e6718e8
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/ScriptChain.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/Tiled Upscaler - Node Example.gif b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/Tiled Upscaler - Node Example.gif
new file mode 100644
index 0000000000000000000000000000000000000000..4e7775df82103a29270636665b43d1a045cf7e33
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/Tiled Upscaler - Node Example.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5cb52aa4fcfdd886aa86807f6a93b1d7ec11d762fbadf9f72770ee797bc9043f
+size 15109903
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/XY Plot - Node Example.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/XY Plot - Node Example.png
new file mode 100644
index 0000000000000000000000000000000000000000..b92f5ea98de7633956d80aa9853474f8bd60670a
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/XY Plot - Node Example.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0fe94a6c59975487dfed24059ff8fcdcc4d706afd572d9a4bc22cf0455dcb20c
+size 1097982
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/readme.md b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/images/nodes/readme.md
@@ -0,0 +1 @@
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/appearance.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/appearance.js
new file mode 100644
index 0000000000000000000000000000000000000000..181033e09b21b2ee2105872e369169a147b14f3f
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/appearance.js
@@ -0,0 +1,99 @@
+import { app } from "../../scripts/app.js";
+
+const COLOR_THEMES = {
+ red: { nodeColor: "#332222", nodeBgColor: "#553333" },
+ green: { nodeColor: "#223322", nodeBgColor: "#335533" },
+ blue: { nodeColor: "#222233", nodeBgColor: "#333355" },
+ pale_blue: { nodeColor: "#2a363b", nodeBgColor: "#3f5159" },
+ cyan: { nodeColor: "#223333", nodeBgColor: "#335555" },
+ purple: { nodeColor: "#332233", nodeBgColor: "#553355" },
+ yellow: { nodeColor: "#443322", nodeBgColor: "#665533" },
+ none: { nodeColor: null, nodeBgColor: null } // no color
+};
+
+const NODE_COLORS = {
+ "KSampler (Efficient)": "random",
+ "KSampler Adv. (Efficient)": "random",
+ "KSampler SDXL (Eff.)": "random",
+ "Efficient Loader": "random",
+ "Eff. Loader SDXL": "random",
+ "LoRA Stacker": "blue",
+ "Control Net Stacker": "green",
+ "Apply ControlNet Stack": "none",
+ "XY Plot": "purple",
+ "Unpack SDXL Tuple": "none",
+ "Pack SDXL Tuple": "none",
+ "XY Input: Seeds++ Batch": "cyan",
+ "XY Input: Add/Return Noise": "cyan",
+ "XY Input: Steps": "cyan",
+ "XY Input: CFG Scale": "cyan",
+ "XY Input: Sampler/Scheduler": "cyan",
+ "XY Input: Denoise": "cyan",
+ "XY Input: VAE": "cyan",
+ "XY Input: Prompt S/R": "cyan",
+ "XY Input: Aesthetic Score": "cyan",
+ "XY Input: Refiner On/Off": "cyan",
+ "XY Input: Checkpoint": "cyan",
+ "XY Input: Clip Skip": "cyan",
+ "XY Input: LoRA": "cyan",
+ "XY Input: LoRA Plot": "cyan",
+ "XY Input: LoRA Stacks": "cyan",
+ "XY Input: Control Net": "cyan",
+ "XY Input: Control Net Plot": "cyan",
+ "XY Input: Manual XY Entry": "cyan",
+ "Manual XY Entry Info": "cyan",
+ "Join XY Inputs of Same Type": "cyan",
+ "Image Overlay": "random",
+ "Noise Control Script": "none",
+ "HighRes-Fix Script": "yellow",
+ "Tiled Upscaler Script": "red",
+ "AnimateDiff Script": "random",
+ "Evaluate Integers": "pale_blue",
+ "Evaluate Floats": "pale_blue",
+ "Evaluate Strings": "pale_blue",
+ "Simple Eval Examples": "pale_blue",
+ };
+
+function shuffleArray(array) {
+ for (let i = array.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]]; // Swap elements
+ }
+}
+
+let colorKeys = Object.keys(COLOR_THEMES).filter(key => key !== "none");
+shuffleArray(colorKeys); // Shuffle the color themes initially
+
+function setNodeColors(node, theme) {
+ if (!theme) {return;}
+ node.shape = "box";
+ if(theme.nodeColor && theme.nodeBgColor) {
+ node.color = theme.nodeColor;
+ node.bgcolor = theme.nodeBgColor;
+ }
+}
+
+const ext = {
+ name: "efficiency.appearance",
+
+ nodeCreated(node) {
+ const nclass = node.comfyClass;
+ if (NODE_COLORS.hasOwnProperty(nclass)) {
+ let colorKey = NODE_COLORS[nclass];
+
+ if (colorKey === "random") {
+ // Check for a valid color key before popping
+ if (colorKeys.length === 0 || !COLOR_THEMES[colorKeys[colorKeys.length - 1]]) {
+ colorKeys = Object.keys(COLOR_THEMES).filter(key => key !== "none");
+ shuffleArray(colorKeys);
+ }
+ colorKey = colorKeys.pop();
+ }
+
+ const theme = COLOR_THEMES[colorKey];
+ setNodeColors(node, theme);
+ }
+ }
+};
+
+app.registerExtension(ext);
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/gif_preview.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/gif_preview.js
new file mode 100644
index 0000000000000000000000000000000000000000..552d0ed75201c4d7d0209d4a649d4ed154d53174
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/gif_preview.js
@@ -0,0 +1,144 @@
+import { app } from '../../scripts/app.js'
+import { api } from '../../scripts/api.js'
+
+function offsetDOMWidget(
+ widget,
+ ctx,
+ node,
+ widgetWidth,
+ widgetY,
+ height
+ ) {
+ const margin = 10
+ const elRect = ctx.canvas.getBoundingClientRect()
+ const transform = new DOMMatrix()
+ .scaleSelf(
+ elRect.width / ctx.canvas.width,
+ elRect.height / ctx.canvas.height
+ )
+ .multiplySelf(ctx.getTransform())
+ .translateSelf(0, widgetY + margin)
+
+ const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
+ Object.assign(widget.inputEl.style, {
+ transformOrigin: '0 0',
+ transform: scale,
+ left: `${transform.e}px`,
+ top: `${transform.d + transform.f}px`,
+ width: `${widgetWidth}px`,
+ height: `${(height || widget.parent?.inputHeight || 32) - margin}px`,
+ position: 'absolute',
+ background: !node.color ? '' : node.color,
+ color: !node.color ? '' : 'white',
+ zIndex: 5, //app.graph._nodes.indexOf(node),
+ })
+ }
+
+ export const hasWidgets = (node) => {
+ if (!node.widgets || !node.widgets?.[Symbol.iterator]) {
+ return false
+ }
+ return true
+ }
+
+ export const cleanupNode = (node) => {
+ if (!hasWidgets(node)) {
+ return
+ }
+
+ for (const w of node.widgets) {
+ if (w.canvas) {
+ w.canvas.remove()
+ }
+ if (w.inputEl) {
+ w.inputEl.remove()
+ }
+ // calls the widget remove callback
+ w.onRemoved?.()
+ }
+ }
+
+const CreatePreviewElement = (name, val, format) => {
+ const [type] = format.split('/')
+ const w = {
+ name,
+ type,
+ value: val,
+ draw: function (ctx, node, widgetWidth, widgetY, height) {
+ const [cw, ch] = this.computeSize(widgetWidth)
+ offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
+ },
+ computeSize: function (_) {
+ const ratio = this.inputRatio || 1
+ const width = Math.max(220, this.parent.size[0])
+ return [width, (width / ratio + 10)]
+ },
+ onRemoved: function () {
+ if (this.inputEl) {
+ this.inputEl.remove()
+ }
+ },
+ }
+
+ w.inputEl = document.createElement(type === 'video' ? 'video' : 'img')
+ w.inputEl.src = w.value
+ if (type === 'video') {
+ w.inputEl.setAttribute('type', 'video/webm');
+ w.inputEl.autoplay = true
+ w.inputEl.loop = true
+ w.inputEl.controls = false;
+ }
+ w.inputEl.onload = function () {
+ w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight
+ }
+ document.body.appendChild(w.inputEl)
+ return w
+ }
+
+const gif_preview = {
+ name: 'efficiency.gif_preview',
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ switch (nodeData.name) {
+ case 'KSampler (Efficient)':{
+ const onExecuted = nodeType.prototype.onExecuted
+ nodeType.prototype.onExecuted = function (message) {
+ const prefix = 'ad_gif_preview_'
+ const r = onExecuted ? onExecuted.apply(this, message) : undefined
+
+ if (this.widgets) {
+ const pos = this.widgets.findIndex((w) => w.name === `${prefix}_0`)
+ if (pos !== -1) {
+ for (let i = pos; i < this.widgets.length; i++) {
+ this.widgets[i].onRemoved?.()
+ }
+ this.widgets.length = pos
+ }
+ if (message?.gifs) {
+ message.gifs.forEach((params, i) => {
+ const previewUrl = api.apiURL(
+ '/view?' + new URLSearchParams(params).toString()
+ )
+ const w = this.addCustomWidget(
+ CreatePreviewElement(`${prefix}_${i}`, previewUrl, params.format || 'image/gif')
+ )
+ w.parent = this
+ })
+ }
+ const onRemoved = this.onRemoved
+ this.onRemoved = () => {
+ cleanupNode(this)
+ return onRemoved?.()
+ }
+ }
+ if (message?.gifs && message.gifs.length > 0) {
+ this.setSize([this.size[0], this.computeSize([this.size[0], this.size[1]])[1]]);
+ }
+ return r
+ }
+ break
+ }
+ }
+ }
+}
+
+app.registerExtension(gif_preview)
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/addLinks.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/addLinks.js
new file mode 100644
index 0000000000000000000000000000000000000000..57555f6f5079118bede60318a1b822384f2dbf98
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/addLinks.js
@@ -0,0 +1,180 @@
+import { app } from "../../../scripts/app.js";
+import { addMenuHandler } from "./common/utils.js";
+import { addNode } from "./common/utils.js";
+
+function createKSamplerEntry(node, samplerType, subNodeType = null, isSDXL = false) {
+ const samplerLabelMap = {
+ "Eff": "KSampler (Efficient)",
+ "Adv": "KSampler Adv. (Efficient)",
+ "SDXL": "KSampler SDXL (Eff.)"
+ };
+
+ const subNodeLabelMap = {
+ "XYPlot": "XY Plot",
+ "NoiseControl": "Noise Control Script",
+ "HiResFix": "HighRes-Fix Script",
+ "TiledUpscale": "Tiled Upscaler Script",
+ "AnimateDiff": "AnimateDiff Script"
+ };
+
+ const nicknameMap = {
+ "KSampler (Efficient)": "KSampler",
+ "KSampler Adv. (Efficient)": "KSampler(Adv)",
+ "KSampler SDXL (Eff.)": "KSampler",
+ "XY Plot": "XY Plot",
+ "Noise Control Script": "NoiseControl",
+ "HighRes-Fix Script": "HiResFix",
+ "Tiled Upscaler Script": "TiledUpscale",
+ "AnimateDiff Script": "AnimateDiff"
+ };
+
+ const kSamplerLabel = samplerLabelMap[samplerType];
+ const subNodeLabel = subNodeLabelMap[subNodeType];
+
+ const kSamplerNickname = nicknameMap[kSamplerLabel];
+ const subNodeNickname = nicknameMap[subNodeLabel];
+
+ const contentLabel = subNodeNickname ? `${kSamplerNickname} + ${subNodeNickname}` : kSamplerNickname;
+
+ return {
+ content: contentLabel,
+ callback: function() {
+ const kSamplerNode = addNode(kSamplerLabel, node, { shiftX: node.size[0] + 50 });
+
+ // Standard connections for all samplers
+ node.connect(0, kSamplerNode, 0); // MODEL
+ node.connect(1, kSamplerNode, 1); // CONDITIONING+
+ node.connect(2, kSamplerNode, 2); // CONDITIONING-
+
+ // Additional connections for non-SDXL
+ if (!isSDXL) {
+ node.connect(3, kSamplerNode, 3); // LATENT
+ node.connect(4, kSamplerNode, 4); // VAE
+ }
+
+ if (subNodeLabel) {
+ const subNode = addNode(subNodeLabel, node, { shiftX: 50, shiftY: node.size[1] + 50 });
+ const dependencyIndex = isSDXL ? 3 : 5;
+ node.connect(dependencyIndex, subNode, 0);
+ subNode.connect(0, kSamplerNode, dependencyIndex);
+ }
+ },
+ };
+}
+
+function createStackerNode(node, type) {
+ const stackerLabelMap = {
+ "LoRA": "LoRA Stacker",
+ "ControlNet": "Control Net Stacker"
+ };
+
+ const contentLabel = stackerLabelMap[type];
+
+ return {
+ content: contentLabel,
+ callback: function() {
+ const stackerNode = addNode(contentLabel, node);
+
+ // Calculate the left shift based on the width of the new node
+ const shiftX = -(stackerNode.size[0] + 25);
+
+ stackerNode.pos[0] += shiftX; // Adjust the x position of the new node
+
+ // Introduce a Y offset of 200 for ControlNet Stacker node
+ if (type === "ControlNet") {
+ stackerNode.pos[1] += 300;
+ }
+
+ // Connect outputs to the Efficient Loader based on type
+ if (type === "LoRA") {
+ stackerNode.connect(0, node, 0);
+ } else if (type === "ControlNet") {
+ stackerNode.connect(0, node, 1);
+ }
+ },
+ };
+}
+
+function createXYPlotNode(node, type) {
+ const contentLabel = "XY Plot";
+
+ return {
+ content: contentLabel,
+ callback: function() {
+ const xyPlotNode = addNode(contentLabel, node);
+
+ // Center the X coordinate of the XY Plot node
+ const centerXShift = (node.size[0] - xyPlotNode.size[0]) / 2;
+ xyPlotNode.pos[0] += centerXShift;
+
+ // Adjust the Y position to place it below the loader node
+ xyPlotNode.pos[1] += node.size[1] + 60;
+
+ // Depending on the node type, connect the appropriate output to the XY Plot node
+ if (type === "Efficient") {
+ node.connect(6, xyPlotNode, 0);
+ } else if (type === "SDXL") {
+ node.connect(3, xyPlotNode, 0);
+ }
+ },
+ };
+}
+
+function getMenuValues(type, node) {
+ const subNodeTypes = [null, "XYPlot", "NoiseControl", "HiResFix", "TiledUpscale", "AnimateDiff"];
+ const excludedSubNodeTypes = ["NoiseControl", "HiResFix", "TiledUpscale", "AnimateDiff"]; // Nodes to exclude from the menu
+
+ const menuValues = [];
+
+ // Add the new node types to the menu first for the correct order
+ menuValues.push(createStackerNode(node, "LoRA"));
+ menuValues.push(createStackerNode(node, "ControlNet"));
+
+ for (const subNodeType of subNodeTypes) {
+ // Skip adding submenu items that are in the excludedSubNodeTypes array
+ if (!excludedSubNodeTypes.includes(subNodeType)) {
+ const menuEntry = createKSamplerEntry(node, type === "Efficient" ? "Eff" : "SDXL", subNodeType, type === "SDXL");
+ menuValues.push(menuEntry);
+ }
+ }
+
+ // Insert the standalone XY Plot option after the KSampler without any subNodeTypes and before any other KSamplers with subNodeTypes
+ menuValues.splice(3, 0, createXYPlotNode(node, type));
+
+ return menuValues;
+}
+
+function showAddLinkMenuCommon(value, options, e, menu, node, type) {
+ const values = getMenuValues(type, node);
+ new LiteGraph.ContextMenu(values, {
+ event: e,
+ callback: null,
+ parentMenu: menu,
+ node: node
+ });
+ return false;
+}
+
+// Extension Definition
+app.registerExtension({
+ name: "efficiency.addLinks",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ const linkTypes = {
+ "Efficient Loader": "Efficient",
+ "Eff. Loader SDXL": "SDXL"
+ };
+
+ const linkType = linkTypes[nodeData.name];
+
+ if (linkType) {
+ addMenuHandler(nodeType, function(insertOption) {
+ insertOption({
+ content: "⛓ Add link...",
+ has_submenu: true,
+ callback: (value, options, e, menu, node) => showAddLinkMenuCommon(value, options, e, menu, node, linkType)
+ });
+ });
+ }
+ },
+});
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/addScripts.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/addScripts.js
new file mode 100644
index 0000000000000000000000000000000000000000..1b22cd97b0f04535b77c3bbf269abdf02a580b6c
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/addScripts.js
@@ -0,0 +1,152 @@
+import { app } from "../../../scripts/app.js";
+import { addMenuHandler } from "./common/utils.js";
+import { addNode } from "./common/utils.js";
+
+const connectionMap = {
+ "KSampler (Efficient)": ["input", 5],
+ "KSampler Adv. (Efficient)": ["input", 5],
+ "KSampler SDXL (Eff.)": ["input", 3],
+ "XY Plot": ["output", 0],
+ "Noise Control Script": ["input & output", 0],
+ "HighRes-Fix Script": ["input & output", 0],
+ "Tiled Upscaler Script": ["input & output", 0],
+ "AnimateDiff Script": ["output", 0]
+};
+
+ /**
+ * connect this node output to the input of another node
+ * @method connect
+ * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
+ * @param {LGraphNode} node the target node
+ * @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger)
+ * @return {Object} the link_info is created, otherwise null
+ LGraphNode.prototype.connect = function(output_slot, target_node, input_slot)
+ **/
+
+function addAndConnectScriptNode(scriptType, selectedNode) {
+ const selectedNodeType = connectionMap[selectedNode.type];
+ const newNodeType = connectionMap[scriptType];
+
+ // 1. Create the new node without position adjustments
+ const newNode = addNode(scriptType, selectedNode, { shiftX: 0, shiftY: 0 });
+
+ // 2. Adjust position of the new node based on conditions
+ if (newNodeType[0].includes("input") && selectedNodeType[0].includes("output")) {
+ newNode.pos[0] += selectedNode.size[0] + 50;
+ } else if (newNodeType[0].includes("output") && selectedNodeType[0].includes("input")) {
+ newNode.pos[0] -= (newNode.size[0] + 50);
+ }
+
+ // 3. Logic for connecting the nodes
+ switch (selectedNodeType[0]) {
+ case "output":
+ if (newNodeType[0] === "input & output") {
+ // For every node that was previously connected to the selectedNode's output
+ const connectedNodes = selectedNode.getOutputNodes(selectedNodeType[1]);
+ if (connectedNodes && connectedNodes.length) {
+ for (let connectedNode of connectedNodes) {
+ // Disconnect the node from selectedNode's output
+ selectedNode.disconnectOutput(selectedNodeType[1]);
+ // Connect the newNode's output to the previously connected node,
+ // using the appropriate slot based on the type of the connectedNode
+ const targetSlot = (connectedNode.type in connectionMap) ? connectionMap[connectedNode.type][1] : 0;
+ newNode.connect(0, connectedNode, targetSlot);
+ }
+ }
+ // Connect selectedNode's output to newNode's input
+ selectedNode.connect(selectedNodeType[1], newNode, newNodeType[1]);
+ }
+ break;
+
+ case "input":
+ if (newNodeType[0] === "output") {
+ newNode.connect(0, selectedNode, selectedNodeType[1]);
+ } else if (newNodeType[0] === "input & output") {
+ const ogInputNode = selectedNode.getInputNode(selectedNodeType[1]);
+ if (ogInputNode) {
+ ogInputNode.connect(0, newNode, 0);
+ }
+ newNode.connect(0, selectedNode, selectedNodeType[1]);
+ }
+ break;
+ case "input & output":
+ if (newNodeType[0] === "output") {
+ newNode.connect(0, selectedNode, 0);
+ } else if (newNodeType[0] === "input & output") {
+
+ const connectedNodes = selectedNode.getOutputNodes(0);
+ if (connectedNodes && connectedNodes.length) {
+ for (let connectedNode of connectedNodes) {
+ selectedNode.disconnectOutput(0);
+ newNode.connect(0, connectedNode, connectedNode.type in connectionMap ? connectionMap[connectedNode.type][1] : 0);
+ }
+ }
+ // Connect selectedNode's output to newNode's input
+ selectedNode.connect(selectedNodeType[1], newNode, newNodeType[1]);
+ }
+ break;
+ }
+
+ return newNode;
+}
+
+function createScriptEntry(node, scriptType) {
+ return {
+ content: scriptType,
+ callback: function() {
+ addAndConnectScriptNode(scriptType, node);
+ },
+ };
+}
+
+function getScriptOptions(nodeType, node) {
+ const allScriptTypes = [
+ "XY Plot",
+ "Noise Control Script",
+ "HighRes-Fix Script",
+ "Tiled Upscaler Script",
+ "AnimateDiff Script"
+ ];
+
+ // Filter script types based on node type
+ const scriptTypes = allScriptTypes.filter(scriptType => {
+ const scriptBehavior = connectionMap[scriptType][0];
+
+ if (connectionMap[nodeType][0] === "output") {
+ return scriptBehavior.includes("input"); // Includes nodes that are "input" or "input & output"
+ } else {
+ return true;
+ }
+ });
+
+ return scriptTypes.map(script => createScriptEntry(node, script));
+}
+
+
+function showAddScriptMenu(_, options, e, menu, node) {
+ const scriptOptions = getScriptOptions(node.type, node);
+ new LiteGraph.ContextMenu(scriptOptions, {
+ event: e,
+ callback: null,
+ parentMenu: menu,
+ node: node
+ });
+ return false;
+}
+
+// Extension Definition
+app.registerExtension({
+ name: "efficiency.addScripts",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (connectionMap[nodeData.name]) {
+ addMenuHandler(nodeType, function(insertOption) {
+ insertOption({
+ content: "📜 Add script...",
+ has_submenu: true,
+ callback: showAddScriptMenu
+ });
+ });
+ }
+ },
+});
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/addXYinputs.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/addXYinputs.js
new file mode 100644
index 0000000000000000000000000000000000000000..2beaf2a56e995c235ecdbcd9aa3d2f678f86159a
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/addXYinputs.js
@@ -0,0 +1,89 @@
+import { app } from "../../../scripts/app.js";
+import { addMenuHandler, addNode } from "./common/utils.js";
+
+const nodePxOffsets = 80;
+
+function getXYInputNodes() {
+ return [
+ "XY Input: Seeds++ Batch",
+ "XY Input: Add/Return Noise",
+ "XY Input: Steps",
+ "XY Input: CFG Scale",
+ "XY Input: Sampler/Scheduler",
+ "XY Input: Denoise",
+ "XY Input: VAE",
+ "XY Input: Prompt S/R",
+ "XY Input: Aesthetic Score",
+ "XY Input: Refiner On/Off",
+ "XY Input: Checkpoint",
+ "XY Input: Clip Skip",
+ "XY Input: LoRA",
+ "XY Input: LoRA Plot",
+ "XY Input: LoRA Stacks",
+ "XY Input: Control Net",
+ "XY Input: Control Net Plot",
+ "XY Input: Manual XY Entry"
+ ];
+}
+
+function showAddXYInputMenu(type, e, menu, node) {
+ const specialNodes = [
+ "XY Input: LoRA Plot",
+ "XY Input: Control Net Plot",
+ "XY Input: Manual XY Entry"
+ ];
+
+ const values = getXYInputNodes().map(nodeType => {
+ return {
+ content: nodeType,
+ callback: function() {
+ const newNode = addNode(nodeType, node);
+
+ // Calculate the left shift based on the width of the new node
+ const shiftX = -(newNode.size[0] + 35);
+ newNode.pos[0] += shiftX;
+
+ if (specialNodes.includes(nodeType)) {
+ newNode.pos[1] += 20;
+ // Connect both outputs to the XY Plot's 2nd and 3rd input.
+ newNode.connect(0, node, 1);
+ newNode.connect(1, node, 2);
+ } else if (type === 'X') {
+ newNode.pos[1] += 20;
+ newNode.connect(0, node, 1); // Connect to 2nd input
+ } else {
+ newNode.pos[1] += node.size[1] + 45;
+ newNode.connect(0, node, 2); // Connect to 3rd input
+ }
+ }
+ };
+ });
+
+ new LiteGraph.ContextMenu(values, {
+ event: e,
+ callback: null,
+ parentMenu: menu,
+ node: node
+ });
+ return false;
+}
+
+app.registerExtension({
+ name: "efficiency.addXYinputs",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === "XY Plot") {
+ addMenuHandler(nodeType, function(insertOption) {
+ insertOption({
+ content: "✏️ Add 𝚇 input...",
+ has_submenu: true,
+ callback: (value, options, e, menu, node) => showAddXYInputMenu('X', e, menu, node)
+ });
+ insertOption({
+ content: "✏️ Add 𝚈 input...",
+ has_submenu: true,
+ callback: (value, options, e, menu, node) => showAddXYInputMenu('Y', e, menu, node)
+ });
+ });
+ }
+ },
+});
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/common/modelInfoDialog.css b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/common/modelInfoDialog.css
new file mode 100644
index 0000000000000000000000000000000000000000..7c9718d030826984e9239bf5298176c7c3f10e16
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/common/modelInfoDialog.css
@@ -0,0 +1,104 @@
+.pysssss-model-info {
+ color: white;
+ font-family: sans-serif;
+ max-width: 90vw;
+}
+.pysssss-model-content {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+.pysssss-model-info h2 {
+ text-align: center;
+ margin: 0 0 10px 0;
+}
+.pysssss-model-info p {
+ margin: 5px 0;
+}
+.pysssss-model-info a {
+ color: dodgerblue;
+}
+.pysssss-model-info a:hover {
+ text-decoration: underline;
+}
+.pysssss-model-tags-list {
+ display: flex;
+ flex-wrap: wrap;
+ list-style: none;
+ gap: 10px;
+ max-height: 200px;
+ overflow: auto;
+ margin: 10px 0;
+ padding: 0;
+}
+.pysssss-model-tag {
+ background-color: rgb(128, 213, 247);
+ color: #000;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ border-radius: 5px;
+ padding: 2px 5px;
+ cursor: pointer;
+}
+.pysssss-model-tag--selected span::before {
+ content: "✅";
+ position: absolute;
+ background-color: dodgerblue;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ text-align: center;
+}
+.pysssss-model-tag:hover {
+ outline: 2px solid dodgerblue;
+}
+.pysssss-model-tag p {
+ margin: 0;
+}
+.pysssss-model-tag span {
+ text-align: center;
+ border-radius: 5px;
+ background-color: dodgerblue;
+ color: #fff;
+ padding: 2px;
+ position: relative;
+ min-width: 20px;
+ overflow: hidden;
+}
+
+.pysssss-model-metadata .comfy-modal-content {
+ max-width: 100%;
+}
+.pysssss-model-metadata label {
+ margin-right: 1ch;
+ color: #ccc;
+}
+
+.pysssss-model-metadata span {
+ color: dodgerblue;
+}
+
+.pysssss-preview {
+ max-width: 50%;
+ margin-left: 10px;
+ position: relative;
+}
+.pysssss-preview img {
+ max-height: 300px;
+}
+.pysssss-preview button {
+ position: absolute;
+ font-size: 12px;
+ bottom: 10px;
+ right: 10px;
+}
+.pysssss-model-notes {
+ background-color: rgba(0, 0, 0, 0.25);
+ padding: 5px;
+ margin-top: 5px;
+}
+.pysssss-model-notes:empty {
+ display: none;
+}
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/common/modelInfoDialog.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/common/modelInfoDialog.js
new file mode 100644
index 0000000000000000000000000000000000000000..9a8c989453f777f4c3e9591c7331651858cf91d3
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/common/modelInfoDialog.js
@@ -0,0 +1,303 @@
+import { $el, ComfyDialog } from "../../../../scripts/ui.js";
+import { api } from "../../../../scripts/api.js";
+import { addStylesheet } from "./utils.js";
+
+addStylesheet(import.meta.url);
+
+class MetadataDialog extends ComfyDialog {
+ constructor() {
+ super();
+
+ this.element.classList.add("pysssss-model-metadata");
+ }
+ show(metadata) {
+ super.show(
+ $el(
+ "div",
+ Object.keys(metadata).map((k) =>
+ $el("div", [$el("label", { textContent: k }), $el("span", { textContent: metadata[k] })])
+ )
+ )
+ );
+ }
+}
+
+export class ModelInfoDialog extends ComfyDialog {
+ constructor(name) {
+ super();
+ this.name = name;
+ this.element.classList.add("pysssss-model-info");
+ }
+
+ get customNotes() {
+ return this.metadata["pysssss.notes"];
+ }
+
+ set customNotes(v) {
+ this.metadata["pysssss.notes"] = v;
+ }
+
+ get hash() {
+ return this.metadata["pysssss.sha256"];
+ }
+
+ async show(type, value) {
+ this.type = type;
+
+ const req = api.fetchApi("/pysssss/metadata/" + encodeURIComponent(`${type}/${value}`));
+ this.info = $el("div", { style: { flex: "auto" } });
+ this.img = $el("img", { style: { display: "none" } });
+ this.imgWrapper = $el("div.pysssss-preview", [this.img]);
+ this.main = $el("main", { style: { display: "flex" } }, [this.info, this.imgWrapper]);
+ this.content = $el("div.pysssss-model-content", [$el("h2", { textContent: this.name }), this.main]);
+
+ const loading = $el("div", { textContent: "ℹ️ Loading...", parent: this.content });
+
+ super.show(this.content);
+
+ this.metadata = await (await req).json();
+ this.viewMetadata.style.cursor = this.viewMetadata.style.opacity = "";
+ this.viewMetadata.removeAttribute("disabled");
+
+ loading.remove();
+ this.addInfo();
+ }
+
+ createButtons() {
+ const btns = super.createButtons();
+ this.viewMetadata = $el("button", {
+ type: "button",
+ textContent: "View raw metadata",
+ disabled: "disabled",
+ style: {
+ opacity: 0.5,
+ cursor: "not-allowed",
+ },
+ onclick: (e) => {
+ if (this.metadata) {
+ new MetadataDialog().show(this.metadata);
+ }
+ },
+ });
+
+ btns.unshift(this.viewMetadata);
+ return btns;
+ }
+
+ getNoteInfo() {
+ function parseNote() {
+ if (!this.customNotes) return [];
+
+ let notes = [];
+ // Extract links from notes
+ const r = new RegExp("(\\bhttps?:\\/\\/[^\\s]+)", "g");
+ let end = 0;
+ let m;
+ do {
+ m = r.exec(this.customNotes);
+ let pos;
+ let fin = 0;
+ if (m) {
+ pos = m.index;
+ fin = m.index + m[0].length;
+ } else {
+ pos = this.customNotes.length;
+ }
+
+ let pre = this.customNotes.substring(end, pos);
+ if (pre) {
+ pre = pre.replaceAll("\n", " ");
+ notes.push(
+ $el("span", {
+ innerHTML: pre,
+ })
+ );
+ }
+ if (m) {
+ notes.push(
+ $el("a", {
+ href: m[0],
+ textContent: m[0],
+ target: "_blank",
+ })
+ );
+ }
+
+ end = fin;
+ } while (m);
+ return notes;
+ }
+
+ let textarea;
+ let notesContainer;
+ const editText = "✏️ Edit";
+ const edit = $el("a", {
+ textContent: editText,
+ href: "#",
+ style: {
+ float: "right",
+ color: "greenyellow",
+ textDecoration: "none",
+ },
+ onclick: async (e) => {
+ e.preventDefault();
+
+ if (textarea) {
+ this.customNotes = textarea.value;
+
+ const resp = await api.fetchApi(
+ "/pysssss/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`),
+ {
+ method: "POST",
+ body: this.customNotes,
+ }
+ );
+
+ if (resp.status !== 200) {
+ console.error(resp);
+ alert(`Error saving notes (${req.status}) ${req.statusText}`);
+ return;
+ }
+
+ e.target.textContent = editText;
+ textarea.remove();
+ textarea = null;
+
+ notesContainer.replaceChildren(...parseNote.call(this));
+ } else {
+ e.target.textContent = "💾 Save";
+ textarea = $el("textarea", {
+ style: {
+ width: "100%",
+ minWidth: "200px",
+ minHeight: "50px",
+ },
+ textContent: this.customNotes,
+ });
+ e.target.after(textarea);
+ notesContainer.replaceChildren();
+ textarea.style.height = Math.min(textarea.scrollHeight, 300) + "px";
+ }
+ },
+ });
+
+ notesContainer = $el("div.pysssss-model-notes", parseNote.call(this));
+ return $el(
+ "div",
+ {
+ style: { display: "contents" },
+ },
+ [edit, notesContainer]
+ );
+ }
+
+ addInfo() {
+ this.addInfoEntry("Notes", this.getNoteInfo());
+ }
+
+ addInfoEntry(name, value) {
+ return $el(
+ "p",
+ {
+ parent: this.info,
+ },
+ [
+ typeof name === "string" ? $el("label", { textContent: name + ": " }) : name,
+ typeof value === "string" ? $el("span", { textContent: value }) : value,
+ ]
+ );
+ }
+
+ async getCivitaiDetails() {
+ const req = await fetch("https://civitai.com/api/v1/model-versions/by-hash/" + this.hash);
+ if (req.status === 200) {
+ return await req.json();
+ } else if (req.status === 404) {
+ throw new Error("Model not found");
+ } else {
+ throw new Error(`Error loading info (${req.status}) ${req.statusText}`);
+ }
+ }
+
+ addCivitaiInfo() {
+ const promise = this.getCivitaiDetails();
+ const content = $el("span", { textContent: "ℹ️ Loading..." });
+
+ this.addInfoEntry(
+ $el("label", [
+ $el("img", {
+ style: {
+ width: "18px",
+ position: "relative",
+ top: "3px",
+ margin: "0 5px 0 0",
+ },
+ src: "https://civitai.com/favicon.ico",
+ }),
+ $el("span", { textContent: "Civitai: " }),
+ ]),
+ content
+ );
+
+ return promise
+ .then((info) => {
+ content.replaceChildren(
+ $el("a", {
+ href: "https://civitai.com/models/" + info.modelId,
+ textContent: "View " + info.model.name,
+ target: "_blank",
+ })
+ );
+
+ if (info.images?.length) {
+ this.img.src = info.images[0].url;
+ this.img.style.display = "";
+
+ this.imgSave = $el("button", {
+ textContent: "Use as preview",
+ parent: this.imgWrapper,
+ onclick: async () => {
+ // Convert the preview to a blob
+ const blob = await (await fetch(this.img.src)).blob();
+
+ // Store it in temp
+ const name = "temp_preview." + new URL(this.img.src).pathname.split(".")[1];
+ const body = new FormData();
+ body.append("image", new File([blob], name));
+ body.append("overwrite", "true");
+ body.append("type", "temp");
+
+ const resp = await api.fetchApi("/upload/image", {
+ method: "POST",
+ body,
+ });
+
+ if (resp.status !== 200) {
+ console.error(resp);
+ alert(`Error saving preview (${req.status}) ${req.statusText}`);
+ return;
+ }
+
+ // Use as preview
+ await api.fetchApi("/pysssss/save/" + encodeURIComponent(`${this.type}/${this.name}`), {
+ method: "POST",
+ body: JSON.stringify({
+ filename: name,
+ type: "temp",
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ });
+ app.refreshComboInNodes();
+ },
+ });
+ }
+
+ return info;
+ })
+ .catch((err) => {
+ content.textContent = "⚠️ " + err.message;
+ });
+ }
+}
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/common/utils.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/common/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..1ea55b1461022da1bed8ecca8145ced19703c4ad
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/common/utils.js
@@ -0,0 +1,94 @@
+import { app } from '../../../../scripts/app.js'
+import { $el } from "../../../../scripts/ui.js";
+
+export function addStylesheet(url) {
+ if (url.endsWith(".js")) {
+ url = url.substr(0, url.length - 2) + "css";
+ }
+ $el("link", {
+ parent: document.head,
+ rel: "stylesheet",
+ type: "text/css",
+ href: url.startsWith("http") ? url : getUrl(url),
+ });
+}
+
+export function getUrl(path, baseUrl) {
+ if (baseUrl) {
+ return new URL(path, baseUrl).toString();
+ } else {
+ return new URL("../" + path, import.meta.url).toString();
+ }
+}
+
+export async function loadImage(url) {
+ return new Promise((res, rej) => {
+ const img = new Image();
+ img.onload = res;
+ img.onerror = rej;
+ img.src = url;
+ });
+}
+
+export function addMenuHandler(nodeType, cb) {
+
+ const GROUPED_MENU_ORDER = {
+ "🔄 Swap with...": 0,
+ "⛓ Add link...": 1,
+ "📜 Add script...": 2,
+ "🔍 View model info...": 3,
+ "🌱 Seed behavior...": 4,
+ "📐 Set Resolution...": 5,
+ "✏️ Add 𝚇 input...": 6,
+ "✏️ Add 𝚈 input...": 7
+ };
+
+ const originalGetOpts = nodeType.prototype.getExtraMenuOptions;
+
+ nodeType.prototype.getExtraMenuOptions = function () {
+ let r = originalGetOpts ? originalGetOpts.apply(this, arguments) || [] : [];
+
+ const insertOption = (option) => {
+ if (GROUPED_MENU_ORDER.hasOwnProperty(option.content)) {
+ // Find the right position for the option
+ let targetPos = r.length; // default to the end
+
+ for (let i = 0; i < r.length; i++) {
+ if (GROUPED_MENU_ORDER.hasOwnProperty(r[i].content) &&
+ GROUPED_MENU_ORDER[option.content] < GROUPED_MENU_ORDER[r[i].content]) {
+ targetPos = i;
+ break;
+ }
+ }
+ // Insert the option at the determined position
+ r.splice(targetPos, 0, option);
+ } else {
+ // If the option is not in the GROUPED_MENU_ORDER, simply add it to the end
+ r.push(option);
+ }
+ };
+
+ cb.call(this, insertOption);
+
+ return r;
+ };
+}
+
+export function findWidgetByName(node, widgetName) {
+ return node.widgets.find(widget => widget.name === widgetName);
+}
+
+// Utility functions
+export function addNode(name, nextTo, options) {
+ options = { select: true, shiftX: 0, shiftY: 0, before: false, ...(options || {}) };
+ const node = LiteGraph.createNode(name);
+ app.graph.add(node);
+ node.pos = [
+ nextTo.pos[0] + options.shiftX,
+ nextTo.pos[1] + options.shiftY,
+ ];
+ if (options.select) {
+ app.canvas.selectNode(node, false);
+ }
+ return node;
+}
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/modelInfo.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/modelInfo.js
new file mode 100644
index 0000000000000000000000000000000000000000..de167e92172a5f0318126a427e872cb820a30c1e
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/modelInfo.js
@@ -0,0 +1,336 @@
+import { app } from "../../../scripts/app.js";
+import { $el } from "../../../scripts/ui.js";
+import { ModelInfoDialog } from "./common/modelInfoDialog.js";
+import { addMenuHandler } from "./common/utils.js";
+
+const MAX_TAGS = 500;
+
+class LoraInfoDialog extends ModelInfoDialog {
+ getTagFrequency() {
+ if (!this.metadata.ss_tag_frequency) return [];
+
+ const datasets = JSON.parse(this.metadata.ss_tag_frequency);
+ const tags = {};
+ for (const setName in datasets) {
+ const set = datasets[setName];
+ for (const t in set) {
+ if (t in tags) {
+ tags[t] += set[t];
+ } else {
+ tags[t] = set[t];
+ }
+ }
+ }
+
+ return Object.entries(tags).sort((a, b) => b[1] - a[1]);
+ }
+
+ getResolutions() {
+ let res = [];
+ if (this.metadata.ss_bucket_info) {
+ const parsed = JSON.parse(this.metadata.ss_bucket_info);
+ if (parsed?.buckets) {
+ for (const { resolution, count } of Object.values(parsed.buckets)) {
+ res.push([count, `${resolution.join("x")} * ${count}`]);
+ }
+ }
+ }
+ res = res.sort((a, b) => b[0] - a[0]).map((a) => a[1]);
+ let r = this.metadata.ss_resolution;
+ if (r) {
+ const s = r.split(",");
+ const w = s[0].replace("(", "");
+ const h = s[1].replace(")", "");
+ res.push(`${w.trim()}x${h.trim()} (Base res)`);
+ } else if ((r = this.metadata["modelspec.resolution"])) {
+ res.push(r + " (Base res");
+ }
+ if (!res.length) {
+ res.push("⚠️ Unknown");
+ }
+ return res;
+ }
+
+ getTagList(tags) {
+ return tags.map((t) =>
+ $el(
+ "li.pysssss-model-tag",
+ {
+ dataset: {
+ tag: t[0],
+ },
+ $: (el) => {
+ el.onclick = () => {
+ el.classList.toggle("pysssss-model-tag--selected");
+ };
+ },
+ },
+ [
+ $el("p", {
+ textContent: t[0],
+ }),
+ $el("span", {
+ textContent: t[1],
+ }),
+ ]
+ )
+ );
+ }
+
+ addTags() {
+ let tags = this.getTagFrequency();
+ let hasMore;
+ if (tags?.length) {
+ const c = tags.length;
+ let list;
+ if (c > MAX_TAGS) {
+ tags = tags.slice(0, MAX_TAGS);
+ hasMore = $el("p", [
+ $el("span", { textContent: `⚠️ Only showing first ${MAX_TAGS} tags ` }),
+ $el("a", {
+ href: "#",
+ textContent: `Show all ${c}`,
+ onclick: () => {
+ list.replaceChildren(...this.getTagList(this.getTagFrequency()));
+ hasMore.remove();
+ },
+ }),
+ ]);
+ }
+ list = $el("ol.pysssss-model-tags-list", this.getTagList(tags));
+ this.tags = $el("div", [list]);
+ } else {
+ this.tags = $el("p", { textContent: "⚠️ No tag frequency metadata found" });
+ }
+
+ this.content.append(this.tags);
+
+ if (hasMore) {
+ this.content.append(hasMore);
+ }
+ }
+
+ async addInfo() {
+ this.addInfoEntry("Name", this.metadata.ss_output_name || "⚠️ Unknown");
+ this.addInfoEntry("Base Model", this.metadata.ss_sd_model_name || "⚠️ Unknown");
+ this.addInfoEntry("Clip Skip", this.metadata.ss_clip_skip || "⚠️ Unknown");
+
+ this.addInfoEntry(
+ "Resolution",
+ $el(
+ "select",
+ this.getResolutions().map((r) => $el("option", { textContent: r }))
+ )
+ );
+
+ super.addInfo();
+ const p = this.addCivitaiInfo();
+ this.addTags();
+
+ const info = await p;
+ if (info) {
+ $el(
+ "p",
+ {
+ parent: this.content,
+ textContent: "Trained Words: ",
+ },
+ [
+ $el("pre", {
+ textContent: info.trainedWords.join(", "),
+ style: {
+ whiteSpace: "pre-wrap",
+ margin: "10px 0",
+ background: "#222",
+ padding: "5px",
+ borderRadius: "5px",
+ maxHeight: "250px",
+ overflow: "auto",
+ },
+ }),
+ ]
+ );
+ $el("div", {
+ parent: this.content,
+ innerHTML: info.description,
+ style: {
+ maxHeight: "250px",
+ overflow: "auto",
+ },
+ });
+ }
+ }
+
+ createButtons() {
+ const btns = super.createButtons();
+
+ function copyTags(e, tags) {
+ const textarea = $el("textarea", {
+ parent: document.body,
+ style: {
+ position: "fixed",
+ },
+ textContent: tags.map((el) => el.dataset.tag).join(", "),
+ });
+ textarea.select();
+ try {
+ document.execCommand("copy");
+ if (!e.target.dataset.text) {
+ e.target.dataset.text = e.target.textContent;
+ }
+ e.target.textContent = "Copied " + tags.length + " tags";
+ setTimeout(() => {
+ e.target.textContent = e.target.dataset.text;
+ }, 1000);
+ } catch (ex) {
+ prompt("Copy to clipboard: Ctrl+C, Enter", text);
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ }
+
+ btns.unshift(
+ $el("button", {
+ type: "button",
+ textContent: "Copy Selected",
+ onclick: (e) => {
+ copyTags(e, [...this.tags.querySelectorAll(".pysssss-model-tag--selected")]);
+ },
+ }),
+ $el("button", {
+ type: "button",
+ textContent: "Copy All",
+ onclick: (e) => {
+ copyTags(e, [...this.tags.querySelectorAll(".pysssss-model-tag")]);
+ },
+ })
+ );
+
+ return btns;
+ }
+}
+
+class CheckpointInfoDialog extends ModelInfoDialog {
+ async addInfo() {
+ super.addInfo();
+ const info = await this.addCivitaiInfo();
+ if (info) {
+ this.addInfoEntry("Base Model", info.baseModel || "⚠️ Unknown");
+
+ $el("div", {
+ parent: this.content,
+ innerHTML: info.description,
+ style: {
+ maxHeight: "250px",
+ overflow: "auto",
+ },
+ });
+ }
+ }
+}
+
+const generateNames = (prefix, start, end) => {
+ const result = [];
+ if (start < end) {
+ for (let i = start; i <= end; i++) {
+ result.push(`${prefix}${i}`);
+ }
+ } else {
+ for (let i = start; i >= end; i--) {
+ result.push(`${prefix}${i}`);
+ }
+ }
+ return result
+}
+
+// NOTE: Orders reversed so they appear in ascending order
+const infoHandler = {
+ "Efficient Loader": {
+ "loras": ["lora_name"],
+ "checkpoints": ["ckpt_name"]
+ },
+ "Eff. Loader SDXL": {
+ "checkpoints": ["refiner_ckpt_name", "base_ckpt_name"]
+ },
+ "LoRA Stacker": {
+ "loras": generateNames("lora_name_", 50, 1)
+ },
+ "XY Input: LoRA": {
+ "loras": generateNames("lora_name_", 50, 1)
+ },
+ "HighRes-Fix Script": {
+ "checkpoints": ["hires_ckpt_name"]
+ }
+};
+
+// Utility functions and other parts of your code remain unchanged
+
+app.registerExtension({
+ name: "efficiency.ModelInfo",
+ beforeRegisterNodeDef(nodeType) {
+ const types = infoHandler[nodeType.comfyClass];
+
+ if (types) {
+ addMenuHandler(nodeType, function (insertOption) { // Here, we are calling addMenuHandler
+ let submenuItems = []; // to store submenu items
+
+ const addSubMenuOption = (type, widgetNames) => {
+ widgetNames.forEach(widgetName => {
+ const widgetValue = this.widgets.find(w => w.name === widgetName)?.value;
+
+ // Check if widgetValue is "None"
+ if (!widgetValue || widgetValue === "None") {
+ return;
+ }
+
+ let value = widgetValue;
+ if (value.content) {
+ value = value.content;
+ }
+ const cls = type === "loras" ? LoraInfoDialog : CheckpointInfoDialog;
+
+ const label = widgetName;
+
+ // Push to submenuItems
+ submenuItems.push({
+ content: label,
+ callback: async () => {
+ new cls(value).show(type, value);
+ },
+ });
+ });
+ };
+
+ if (typeof types === 'object') {
+ Object.keys(types).forEach(type => {
+ addSubMenuOption(type, types[type]);
+ });
+ }
+
+ // If we have submenu items, use insertOption
+ if (submenuItems.length) {
+ insertOption({ // Using insertOption here
+ content: "🔍 View model info...",
+ has_submenu: true,
+ callback: (value, options, e, menu, node) => {
+ new LiteGraph.ContextMenu(submenuItems, {
+ event: e,
+ callback: null,
+ parentMenu: menu,
+ node: node
+ });
+
+ return false; // This ensures the original context menu doesn't proceed
+ }
+ });
+ }
+ });
+ }
+ },
+});
+
+
+
+
+
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/setResolution.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/setResolution.js
new file mode 100644
index 0000000000000000000000000000000000000000..c65ee8b4af6555b66017d28f4b641b180d10e033
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/setResolution.js
@@ -0,0 +1,88 @@
+// Additional functions and imports
+import { app } from "../../../scripts/app.js";
+import { addMenuHandler, findWidgetByName } from "./common/utils.js";
+
+// A mapping for resolutions based on the type of the loader
+const RESOLUTIONS = {
+ "Efficient Loader": [
+ {width: 512, height: 512},
+ {width: 512, height: 768},
+ {width: 512, height: 640},
+ {width: 640, height: 512},
+ {width: 640, height: 768},
+ {width: 640, height: 640},
+ {width: 768, height: 512},
+ {width: 768, height: 768},
+ {width: 768, height: 640},
+ ],
+ "Eff. Loader SDXL": [
+ {width: 1024, height: 1024},
+ {width: 1152, height: 896},
+ {width: 896, height: 1152},
+ {width: 1216, height: 832},
+ {width: 832, height: 1216},
+ {width: 1344, height: 768},
+ {width: 768, height: 1344},
+ {width: 1536, height: 640},
+ {width: 640, height: 1536}
+ ]
+};
+
+// Function to set the resolution of a node
+function setNodeResolution(node, width, height) {
+ let widthWidget = findWidgetByName(node, "empty_latent_width");
+ let heightWidget = findWidgetByName(node, "empty_latent_height");
+
+ if (widthWidget) {
+ widthWidget.value = width;
+ }
+
+ if (heightWidget) {
+ heightWidget.value = height;
+ }
+}
+
+// The callback for the resolution submenu
+function resolutionMenuCallback(node, width, height) {
+ return function() {
+ setNodeResolution(node, width, height);
+ };
+}
+
+// Show the set resolution submenu
+function showResolutionMenu(value, options, e, menu, node) {
+ const resolutions = RESOLUTIONS[node.type];
+ if (!resolutions) {
+ return false;
+ }
+
+ const resolutionOptions = resolutions.map(res => ({
+ content: `${res.width} x ${res.height}`,
+ callback: resolutionMenuCallback(node, res.width, res.height)
+ }));
+
+ new LiteGraph.ContextMenu(resolutionOptions, {
+ event: e,
+ callback: null,
+ parentMenu: menu,
+ node: node
+ });
+
+ return false; // This ensures the original context menu doesn't proceed
+}
+
+// Extension Definition
+app.registerExtension({
+ name: "efficiency.SetResolution",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (["Efficient Loader", "Eff. Loader SDXL"].includes(nodeData.name)) {
+ addMenuHandler(nodeType, function (insertOption) {
+ insertOption({
+ content: "📐 Set Resolution...",
+ has_submenu: true,
+ callback: showResolutionMenu
+ });
+ });
+ }
+ },
+});
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapLoaders.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapLoaders.js
new file mode 100644
index 0000000000000000000000000000000000000000..6e051e67185d6c7a1eae53525353e59fa7eaf20a
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapLoaders.js
@@ -0,0 +1,135 @@
+import { app } from "../../../scripts/app.js";
+import { addMenuHandler } from "./common/utils.js";
+import { findWidgetByName } from "./common/utils.js";
+
+function replaceNode(oldNode, newNodeName) {
+ const newNode = LiteGraph.createNode(newNodeName);
+ if (!newNode) {
+ return;
+ }
+ app.graph.add(newNode);
+
+ newNode.pos = oldNode.pos.slice();
+ newNode.size = oldNode.size.slice();
+
+ // Transfer widget values
+ const widgetMapping = {
+ "ckpt_name": "base_ckpt_name",
+ "vae_name": "vae_name",
+ "clip_skip": "base_clip_skip",
+ "positive": "positive",
+ "negative": "negative",
+ "prompt_style": "prompt_style",
+ "empty_latent_width": "empty_latent_width",
+ "empty_latent_height": "empty_latent_height",
+ "batch_size": "batch_size"
+ };
+
+ let effectiveWidgetMapping = widgetMapping;
+
+ // Invert the mapping when going from "Eff. Loader SDXL" to "Efficient Loader"
+ if (oldNode.type === "Eff. Loader SDXL" && newNodeName === "Efficient Loader") {
+ effectiveWidgetMapping = {};
+ for (const [key, value] of Object.entries(widgetMapping)) {
+ effectiveWidgetMapping[value] = key;
+ }
+ }
+
+ oldNode.widgets.forEach(widget => {
+ const newName = effectiveWidgetMapping[widget.name];
+ if (newName) {
+ const newWidget = findWidgetByName(newNode, newName);
+ if (newWidget) {
+ newWidget.value = widget.value;
+ }
+ }
+ });
+
+ // Hardcoded transfer for specific outputs based on the output names from the nodes in the image
+ const outputMapping = {
+ "MODEL": null, // Not present in "Eff. Loader SDXL"
+ "CONDITIONING+": null, // Not present in "Eff. Loader SDXL"
+ "CONDITIONING-": null, // Not present in "Eff. Loader SDXL"
+ "LATENT": "LATENT",
+ "VAE": "VAE",
+ "CLIP": null, // Not present in "Eff. Loader SDXL"
+ "DEPENDENCIES": "DEPENDENCIES"
+ };
+
+ // Transfer connections from old node outputs to new node outputs based on the outputMapping
+ oldNode.outputs.forEach((output, index) => {
+ if (output && output.links && outputMapping[output.name]) {
+ const newOutputName = outputMapping[output.name];
+
+ // If the new node does not have this output, skip
+ if (newOutputName === null) {
+ return;
+ }
+
+ const newOutputIndex = newNode.findOutputSlot(newOutputName);
+ if (newOutputIndex !== -1) {
+ output.links.forEach(link => {
+ const targetLinkInfo = oldNode.graph.links[link];
+ if (targetLinkInfo) {
+ const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
+ if (targetNode) {
+ newNode.connect(newOutputIndex, targetNode, targetLinkInfo.target_slot);
+ }
+ }
+ });
+ }
+ }
+ });
+
+ // Remove old node
+ app.graph.remove(oldNode);
+}
+
+function replaceNodeMenuCallback(currentNode, targetNodeName) {
+ return function() {
+ replaceNode(currentNode, targetNodeName);
+ };
+}
+
+function showSwapMenu(value, options, e, menu, node) {
+ const swapOptions = [];
+
+ if (node.type !== "Efficient Loader") {
+ swapOptions.push({
+ content: "Efficient Loader",
+ callback: replaceNodeMenuCallback(node, "Efficient Loader")
+ });
+ }
+
+ if (node.type !== "Eff. Loader SDXL") {
+ swapOptions.push({
+ content: "Eff. Loader SDXL",
+ callback: replaceNodeMenuCallback(node, "Eff. Loader SDXL")
+ });
+ }
+
+ new LiteGraph.ContextMenu(swapOptions, {
+ event: e,
+ callback: null,
+ parentMenu: menu,
+ node: node
+ });
+
+ return false; // This ensures the original context menu doesn't proceed
+}
+
+// Extension Definition
+app.registerExtension({
+ name: "efficiency.SwapLoaders",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (["Efficient Loader", "Eff. Loader SDXL"].includes(nodeData.name)) {
+ addMenuHandler(nodeType, function (insertOption) {
+ insertOption({
+ content: "🔄 Swap with...",
+ has_submenu: true,
+ callback: showSwapMenu
+ });
+ });
+ }
+ },
+});
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapSamplers.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapSamplers.js
new file mode 100644
index 0000000000000000000000000000000000000000..4d28113cf05a5713eb27fe3537a4119d7cf3fbd7
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapSamplers.js
@@ -0,0 +1,191 @@
+import { app } from "../../../scripts/app.js";
+import { addMenuHandler } from "./common/utils.js";
+import { findWidgetByName } from "./common/utils.js";
+
+function replaceNode(oldNode, newNodeName) {
+ // Create new node
+ const newNode = LiteGraph.createNode(newNodeName);
+ if (!newNode) {
+ return;
+ }
+ app.graph.add(newNode);
+
+ // Position new node at the same position as the old node
+ newNode.pos = oldNode.pos.slice();
+
+ // Define widget mappings
+ const mappings = {
+ "KSampler (Efficient) <-> KSampler Adv. (Efficient)": {
+ seed: "noise_seed",
+ cfg: "cfg",
+ sampler_name: "sampler_name",
+ scheduler: "scheduler",
+ preview_method: "preview_method",
+ vae_decode: "vae_decode"
+ },
+ "KSampler (Efficient) <-> KSampler SDXL (Eff.)": {
+ seed: "noise_seed",
+ cfg: "cfg",
+ sampler_name: "sampler_name",
+ scheduler: "scheduler",
+ preview_method: "preview_method",
+ vae_decode: "vae_decode"
+ },
+ "KSampler Adv. (Efficient) <-> KSampler SDXL (Eff.)": {
+ noise_seed: "noise_seed",
+ steps: "steps",
+ cfg: "cfg",
+ sampler_name: "sampler_name",
+ scheduler: "scheduler",
+ start_at_step: "start_at_step",
+ preview_method: "preview_method",
+ vae_decode: "vae_decode"}
+ };
+
+ const swapKey = `${oldNode.type} <-> ${newNodeName}`;
+
+ let widgetMapping = {};
+
+ // Check if a reverse mapping is needed
+ if (!mappings[swapKey]) {
+ const reverseKey = `${newNodeName} <-> ${oldNode.type}`;
+ const reverseMapping = mappings[reverseKey];
+ if (reverseMapping) {
+ widgetMapping = Object.entries(reverseMapping).reduce((acc, [key, value]) => {
+ acc[value] = key;
+ return acc;
+ }, {});
+ }
+ } else {
+ widgetMapping = mappings[swapKey];
+ }
+
+ if (oldNode.type === "KSampler (Efficient)" && (newNodeName === "KSampler Adv. (Efficient)" || newNodeName === "KSampler SDXL (Eff.)")) {
+ const denoise = Math.min(Math.max(findWidgetByName(oldNode, "denoise").value, 0), 1); // Ensure denoise is between 0 and 1
+ const steps = Math.min(Math.max(findWidgetByName(oldNode, "steps").value, 0), 10000); // Ensure steps is between 0 and 10000
+
+ const total_steps = Math.floor(steps / denoise);
+ const start_at_step = total_steps - steps;
+
+ findWidgetByName(newNode, "steps").value = Math.min(Math.max(total_steps, 0), 10000); // Ensure total_steps is between 0 and 10000
+ findWidgetByName(newNode, "start_at_step").value = Math.min(Math.max(start_at_step, 0), 10000); // Ensure start_at_step is between 0 and 10000
+ }
+ else if ((oldNode.type === "KSampler Adv. (Efficient)" || oldNode.type === "KSampler SDXL (Eff.)") && newNodeName === "KSampler (Efficient)") {
+ const stepsAdv = Math.min(Math.max(findWidgetByName(oldNode, "steps").value, 0), 10000); // Ensure stepsAdv is between 0 and 10000
+ const start_at_step = Math.min(Math.max(findWidgetByName(oldNode, "start_at_step").value, 0), 10000); // Ensure start_at_step is between 0 and 10000
+
+ const denoise = Math.min(Math.max((stepsAdv - start_at_step) / stepsAdv, 0), 1); // Ensure denoise is between 0 and 1
+ const stepsTotal = stepsAdv - start_at_step;
+
+ findWidgetByName(newNode, "denoise").value = denoise;
+ findWidgetByName(newNode, "steps").value = Math.min(Math.max(stepsTotal, 0), 10000); // Ensure stepsTotal is between 0 and 10000
+ }
+
+ // Transfer widget values from old node to new node
+ oldNode.widgets.forEach(widget => {
+ const newName = widgetMapping[widget.name];
+ if (newName) {
+ const newWidget = findWidgetByName(newNode, newName);
+ if (newWidget) {
+ newWidget.value = widget.value;
+ }
+ }
+ });
+
+ // Determine the starting indices based on the node types
+ let oldNodeInputStartIndex = 0;
+ let newNodeInputStartIndex = 0;
+ let oldNodeOutputStartIndex = 0;
+ let newNodeOutputStartIndex = 0;
+
+ if (oldNode.type === "KSampler SDXL (Eff.)" || newNodeName === "KSampler SDXL (Eff.)") {
+ oldNodeInputStartIndex = (oldNode.type === "KSampler SDXL (Eff.)") ? 1 : 3;
+ newNodeInputStartIndex = (newNodeName === "KSampler SDXL (Eff.)") ? 1 : 3;
+ oldNodeOutputStartIndex = (oldNode.type === "KSampler SDXL (Eff.)") ? 1 : 3;
+ newNodeOutputStartIndex = (newNodeName === "KSampler SDXL (Eff.)") ? 1 : 3;
+ }
+
+ // Transfer connections from old node to new node
+ oldNode.inputs.slice(oldNodeInputStartIndex).forEach((input, index) => {
+ if (input && input.link !== null) {
+ const originLinkInfo = oldNode.graph.links[input.link];
+ if (originLinkInfo) {
+ const originNode = oldNode.graph.getNodeById(originLinkInfo.origin_id);
+ if (originNode) {
+ originNode.connect(originLinkInfo.origin_slot, newNode, index + newNodeInputStartIndex);
+ }
+ }
+ }
+ });
+
+ oldNode.outputs.slice(oldNodeOutputStartIndex).forEach((output, index) => {
+ if (output && output.links) {
+ output.links.forEach(link => {
+ const targetLinkInfo = oldNode.graph.links[link];
+ if (targetLinkInfo) {
+ const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
+ if (targetNode) {
+ newNode.connect(index + newNodeOutputStartIndex, targetNode, targetLinkInfo.target_slot);
+ }
+ }
+ });
+ }
+ });
+
+ // Remove old node
+ app.graph.remove(oldNode);
+}
+
+function replaceNodeMenuCallback(currentNode, targetNodeName) {
+ return function() {
+ replaceNode(currentNode, targetNodeName);
+ };
+}
+
+function showSwapMenu(value, options, e, menu, node) {
+ const swapOptions = [];
+
+ if (node.type !== "KSampler (Efficient)") {
+ swapOptions.push({
+ content: "KSampler (Efficient)",
+ callback: replaceNodeMenuCallback(node, "KSampler (Efficient)")
+ });
+ }
+ if (node.type !== "KSampler Adv. (Efficient)") {
+ swapOptions.push({
+ content: "KSampler Adv. (Efficient)",
+ callback: replaceNodeMenuCallback(node, "KSampler Adv. (Efficient)")
+ });
+ }
+ if (node.type !== "KSampler SDXL (Eff.)") {
+ swapOptions.push({
+ content: "KSampler SDXL (Eff.)",
+ callback: replaceNodeMenuCallback(node, "KSampler SDXL (Eff.)")
+ });
+ }
+
+ new LiteGraph.ContextMenu(swapOptions, {
+ event: e,
+ callback: null,
+ parentMenu: menu,
+ node: node
+ });
+
+ return false; // This ensures the original context menu doesn't proceed
+}
+
+// Extension Definition
+app.registerExtension({
+ name: "efficiency.SwapSamplers",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (["KSampler (Efficient)", "KSampler Adv. (Efficient)", "KSampler SDXL (Eff.)"].includes(nodeData.name)) {
+ addMenuHandler(nodeType, function (insertOption) {
+ insertOption({
+ content: "🔄 Swap with...",
+ has_submenu: true,
+ callback: showSwapMenu
+ });
+ });
+ }
+ },
+});
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapScripts.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapScripts.js
new file mode 100644
index 0000000000000000000000000000000000000000..a0aee9bb4168732480c75eccaf6bfa73b80a1a9c
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapScripts.js
@@ -0,0 +1,100 @@
+import { app } from "../../../scripts/app.js";
+import { addMenuHandler } from "./common/utils.js";
+
+function replaceNode(oldNode, newNodeName) {
+ const newNode = LiteGraph.createNode(newNodeName);
+ if (!newNode) {
+ return;
+ }
+ app.graph.add(newNode);
+
+ newNode.pos = oldNode.pos.slice();
+
+ // Transfer connections from old node to new node
+ // XY Plot and AnimateDiff have only one output
+ if(["XY Plot", "AnimateDiff Script"].includes(oldNode.type)) {
+ if (oldNode.outputs[0] && oldNode.outputs[0].links) {
+ oldNode.outputs[0].links.forEach(link => {
+ const targetLinkInfo = oldNode.graph.links[link];
+ if (targetLinkInfo) {
+ const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
+ if (targetNode) {
+ newNode.connect(0, targetNode, targetLinkInfo.target_slot);
+ }
+ }
+ });
+ }
+ } else {
+ // Noise Control Script, HighRes-Fix Script, and Tiled Upscaler Script have 1 input and 1 output at index 0
+ if (oldNode.inputs[0] && oldNode.inputs[0].link !== null) {
+ const originLinkInfo = oldNode.graph.links[oldNode.inputs[0].link];
+ if (originLinkInfo) {
+ const originNode = oldNode.graph.getNodeById(originLinkInfo.origin_id);
+ if (originNode) {
+ originNode.connect(originLinkInfo.origin_slot, newNode, 0);
+ }
+ }
+ }
+
+ if (oldNode.outputs[0] && oldNode.outputs[0].links) {
+ oldNode.outputs[0].links.forEach(link => {
+ const targetLinkInfo = oldNode.graph.links[link];
+ if (targetLinkInfo) {
+ const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
+ if (targetNode) {
+ newNode.connect(0, targetNode, targetLinkInfo.target_slot);
+ }
+ }
+ });
+ }
+ }
+
+ // Remove old node
+ app.graph.remove(oldNode);
+}
+
+function replaceNodeMenuCallback(currentNode, targetNodeName) {
+ return function() {
+ replaceNode(currentNode, targetNodeName);
+ };
+}
+
+function showSwapMenu(value, options, e, menu, node) {
+ const scriptNodes = [
+ "XY Plot",
+ "Noise Control Script",
+ "HighRes-Fix Script",
+ "Tiled Upscaler Script",
+ "AnimateDiff Script"
+ ];
+
+ const swapOptions = scriptNodes.filter(n => n !== node.type).map(n => ({
+ content: n,
+ callback: replaceNodeMenuCallback(node, n)
+ }));
+
+ new LiteGraph.ContextMenu(swapOptions, {
+ event: e,
+ callback: null,
+ parentMenu: menu,
+ node: node
+ });
+
+ return false;
+}
+
+// Extension Definition
+app.registerExtension({
+ name: "efficiency.SwapScripts",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (["XY Plot", "Noise Control Script", "HighRes-Fix Script", "Tiled Upscaler Script", "AnimateDiff Script"].includes(nodeData.name)) {
+ addMenuHandler(nodeType, function (insertOption) {
+ insertOption({
+ content: "🔄 Swap with...",
+ has_submenu: true,
+ callback: showSwapMenu
+ });
+ });
+ }
+ },
+});
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapXYinputs.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapXYinputs.js
new file mode 100644
index 0000000000000000000000000000000000000000..218ba61fce86ad1c363621f5274db417a26e0be9
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/node_options/swapXYinputs.js
@@ -0,0 +1,98 @@
+import { app } from "../../../scripts/app.js";
+import { addMenuHandler } from "./common/utils.js";
+
+function replaceNode(oldNode, newNodeName) {
+ const newNode = LiteGraph.createNode(newNodeName);
+ if (!newNode) {
+ return;
+ }
+ app.graph.add(newNode);
+
+ newNode.pos = oldNode.pos.slice();
+
+ // Handle the special nodes with two outputs
+ const nodesWithTwoOutputs = ["XY Input: LoRA Plot", "XY Input: Control Net Plot", "XY Input: Manual XY Entry"];
+ let outputCount = nodesWithTwoOutputs.includes(oldNode.type) ? 2 : 1;
+
+ // Transfer output connections from old node to new node
+ oldNode.outputs.slice(0, outputCount).forEach((output, index) => {
+ if (output && output.links) {
+ output.links.forEach(link => {
+ const targetLinkInfo = oldNode.graph.links[link];
+ if (targetLinkInfo) {
+ const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
+ if (targetNode) {
+ newNode.connect(index, targetNode, targetLinkInfo.target_slot);
+ }
+ }
+ });
+ }
+ });
+
+ // Remove old node
+ app.graph.remove(oldNode);
+}
+
+function replaceNodeMenuCallback(currentNode, targetNodeName) {
+ return function() {
+ replaceNode(currentNode, targetNodeName);
+ };
+}
+
+function showSwapMenu(value, options, e, menu, node) {
+ const swapOptions = [];
+ const xyInputNodes = [
+ "XY Input: Seeds++ Batch",
+ "XY Input: Add/Return Noise",
+ "XY Input: Steps",
+ "XY Input: CFG Scale",
+ "XY Input: Sampler/Scheduler",
+ "XY Input: Denoise",
+ "XY Input: VAE",
+ "XY Input: Prompt S/R",
+ "XY Input: Aesthetic Score",
+ "XY Input: Refiner On/Off",
+ "XY Input: Checkpoint",
+ "XY Input: Clip Skip",
+ "XY Input: LoRA",
+ "XY Input: LoRA Plot",
+ "XY Input: LoRA Stacks",
+ "XY Input: Control Net",
+ "XY Input: Control Net Plot",
+ "XY Input: Manual XY Entry"
+ ];
+
+ for (const nodeType of xyInputNodes) {
+ if (node.type !== nodeType) {
+ swapOptions.push({
+ content: nodeType,
+ callback: replaceNodeMenuCallback(node, nodeType)
+ });
+ }
+ }
+
+ new LiteGraph.ContextMenu(swapOptions, {
+ event: e,
+ callback: null,
+ parentMenu: menu,
+ node: node
+ });
+
+ return false; // This ensures the original context menu doesn't proceed
+}
+
+// Extension Definition
+app.registerExtension({
+ name: "efficiency.swapXYinputs",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name.startsWith("XY Input:")) {
+ addMenuHandler(nodeType, function (insertOption) {
+ insertOption({
+ content: "🔄 Swap with...",
+ has_submenu: true,
+ callback: showSwapMenu
+ });
+ });
+ }
+ },
+});
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/previewfix.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/previewfix.js
new file mode 100644
index 0000000000000000000000000000000000000000..fa2b9421645dfa59bb74dbf93fe7aa99d849267d
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/previewfix.js
@@ -0,0 +1,67 @@
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js";
+
+app.registerExtension({
+ name: "efficiency.previewfix",
+ lastExecutedNodeId: null,
+ blobsToRevoke: [], // Array to accumulate blob URLs for revocation
+ debug: false,
+
+ log(...args) {
+ if (this.debug) console.log(...args);
+ },
+
+ error(...args) {
+ if (this.debug) console.error(...args);
+ },
+
+ shouldRevokeBlobForNode(nodeId) {
+ const node = app.graph.getNodeById(nodeId);
+
+ const validTitles = [
+ "KSampler (Efficient)",
+ "KSampler Adv. (Efficient)",
+ "KSampler SDXL (Eff.)"
+ ];
+
+ if (!node || !validTitles.includes(node.title)) {
+ return false;
+ }
+
+ const getValue = name => ((node.widgets || []).find(w => w.name === name) || {}).value;
+ return getValue("preview_method") !== "none" && getValue("vae_decode").includes("true");
+ },
+
+ setup() {
+ // Intercepting blob creation to store and immediately revoke the last blob URL
+ const originalCreateObjectURL = URL.createObjectURL;
+ URL.createObjectURL = (object) => {
+ const blobURL = originalCreateObjectURL(object);
+ if (blobURL.startsWith('blob:')) {
+ this.log("[BlobURLLogger] Blob URL created:", blobURL);
+
+ // If the current node meets the criteria, add the blob URL to the revocation list
+ if (this.shouldRevokeBlobForNode(this.lastExecutedNodeId)) {
+ this.blobsToRevoke.push(blobURL);
+ }
+ }
+ return blobURL;
+ };
+
+ // Listen to the start of the node execution to revoke all accumulated blob URLs
+ api.addEventListener("executing", ({ detail }) => {
+ if (this.lastExecutedNodeId !== detail || detail === null) {
+ this.blobsToRevoke.forEach(blob => {
+ this.log("[BlobURLLogger] Revoking Blob URL:", blob);
+ URL.revokeObjectURL(blob);
+ });
+ this.blobsToRevoke = []; // Clear the list after revoking all blobs
+ }
+
+ // Update the last executed node ID
+ this.lastExecutedNodeId = detail;
+ });
+
+ this.log("[BlobURLLogger] Hook attached.");
+ },
+});
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/seedcontrol.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/seedcontrol.js
new file mode 100644
index 0000000000000000000000000000000000000000..59657fa5dcad8b033d6adaee233f342038a1ca8a
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/seedcontrol.js
@@ -0,0 +1,251 @@
+import { app } from "../../scripts/app.js";
+import { addMenuHandler } from "./node_options/common/utils.js";
+
+const LAST_SEED_BUTTON_LABEL = '🎲 Randomize / ♻️ Last Queued Seed';
+const SEED_BEHAVIOR_RANDOMIZE = 'Randomize';
+const SEED_BEHAVIOR_INCREMENT = 'Increment';
+const SEED_BEHAVIOR_DECREMENT = 'Decrement';
+
+const NODE_WIDGET_MAP = {
+ "KSampler (Efficient)": "seed",
+ "KSampler Adv. (Efficient)": "noise_seed",
+ "KSampler SDXL (Eff.)": "noise_seed",
+ "Noise Control Script": "seed",
+ "HighRes-Fix Script": "seed",
+ "Tiled Upscaler Script": "seed"
+};
+
+const SPECIFIC_WIDTH = 325; // Set to desired width
+
+function setNodeWidthForMappedTitles(node) {
+ if (NODE_WIDGET_MAP[node.comfyClass]) {
+ node.setSize([SPECIFIC_WIDTH, node.size[1]]);
+ }
+}
+
+class SeedControl {
+ constructor(node, seedName) {
+ this.lastSeed = -1;
+ this.serializedCtx = {};
+ this.node = node;
+ this.seedBehavior = 'randomize'; // Default behavior
+
+ let controlAfterGenerateIndex;
+
+ for (const [i, w] of this.node.widgets.entries()) {
+ if (w.name === seedName) {
+ this.seedWidget = w;
+ } else if (w.name === 'control_after_generate') {
+ controlAfterGenerateIndex = i;
+ this.node.widgets.splice(i, 1);
+ }
+ }
+
+ if (!this.seedWidget) {
+ throw new Error('Something\'s wrong; expected seed widget');
+ }
+
+ this.lastSeedButton = this.node.addWidget("button", LAST_SEED_BUTTON_LABEL, null, () => {
+ const isValidValue = Number.isInteger(this.seedWidget.value) && this.seedWidget.value >= min && this.seedWidget.value <= max;
+
+ // Special case: if the current label is the default and seed value is -1
+ if (this.lastSeedButton.name === LAST_SEED_BUTTON_LABEL && this.seedWidget.value == -1) {
+ return; // Do nothing and return early
+ }
+
+ if (isValidValue && this.seedWidget.value != -1) {
+ this.lastSeed = this.seedWidget.value;
+ this.seedWidget.value = -1;
+ } else if (this.lastSeed !== -1) {
+ this.seedWidget.value = this.lastSeed;
+ } else {
+ this.seedWidget.value = -1; // Set to -1 if the label didn't update due to a seed value issue
+ }
+
+ if (isValidValue) {
+ this.updateButtonLabel(); // Update the button label to reflect the change
+ }
+ }, { width: 50, serialize: false });
+
+ setNodeWidthForMappedTitles(node);
+ if (controlAfterGenerateIndex !== undefined) {
+ const addedWidget = this.node.widgets.pop();
+ this.node.widgets.splice(controlAfterGenerateIndex, 0, addedWidget);
+ setNodeWidthForMappedTitles(node);
+ }
+
+ const max = Math.min(1125899906842624, this.seedWidget.options.max);
+ const min = Math.max(-1125899906842624, this.seedWidget.options.min);
+ const range = (max - min) / (this.seedWidget.options.step / 10);
+
+ this.seedWidget.serializeValue = async (node, index) => {
+ // Check if the button is disabled
+ if (this.lastSeedButton.disabled) {
+ return this.seedWidget.value;
+ }
+
+ const currentSeed = this.seedWidget.value;
+ this.serializedCtx = {
+ wasSpecial: currentSeed == -1,
+ };
+
+ if (this.serializedCtx.wasSpecial) {
+ switch (this.seedBehavior) {
+ case 'increment':
+ this.serializedCtx.seedUsed = this.lastSeed + 1;
+ break;
+ case 'decrement':
+ this.serializedCtx.seedUsed = this.lastSeed - 1;
+ break;
+ default:
+ this.serializedCtx.seedUsed = Math.floor(Math.random() * range) * (this.seedWidget.options.step / 10) + min;
+ break;
+ }
+
+ // Ensure the seed value is an integer and remains within the accepted range
+ this.serializedCtx.seedUsed = Number.isInteger(this.serializedCtx.seedUsed) ? Math.min(Math.max(this.serializedCtx.seedUsed, min), max) : this.seedWidget.value;
+
+ } else {
+ this.serializedCtx.seedUsed = this.seedWidget.value;
+ }
+
+ if (node && node.widgets_values) {
+ node.widgets_values[index] = this.serializedCtx.seedUsed;
+ } else {
+ // Update the last seed value and the button's label to show the current seed value
+ this.lastSeed = this.serializedCtx.seedUsed;
+ this.updateButtonLabel();
+ }
+
+ this.seedWidget.value = this.serializedCtx.seedUsed;
+
+ if (this.serializedCtx.wasSpecial) {
+ this.lastSeed = this.serializedCtx.seedUsed;
+ this.updateButtonLabel();
+ }
+
+ return this.serializedCtx.seedUsed;
+ };
+
+ this.seedWidget.afterQueued = () => {
+ // Check if the button is disabled
+ if (this.lastSeedButton.disabled) {
+ return; // Exit the function immediately
+ }
+
+ if (this.serializedCtx.wasSpecial) {
+ this.seedWidget.value = -1;
+ }
+
+ // Check if seed has changed to a non -1 value, and if so, update lastSeed
+ if (this.seedWidget.value !== -1) {
+ this.lastSeed = this.seedWidget.value;
+ }
+
+ this.updateButtonLabel();
+ this.serializedCtx = {};
+ };
+ }
+
+ setBehavior(behavior) {
+ this.seedBehavior = behavior;
+
+ // Capture the current seed value as lastSeed and then set the seed widget value to -1
+ if (this.seedWidget.value != -1) {
+ this.lastSeed = this.seedWidget.value;
+ this.seedWidget.value = -1;
+ }
+
+ this.updateButtonLabel();
+ }
+
+ updateButtonLabel() {
+
+ switch (this.seedBehavior) {
+ case 'increment':
+ this.lastSeedButton.name = `➕ Increment / ♻️ ${this.lastSeed === -1 ? "Last Queued Seed" : this.lastSeed}`;
+ break;
+ case 'decrement':
+ this.lastSeedButton.name = `➖ Decrement / ♻️ ${this.lastSeed === -1 ? "Last Queued Seed" : this.lastSeed}`;
+ break;
+ default:
+ this.lastSeedButton.name = `🎲 Randomize / ♻️ ${this.lastSeed === -1 ? "Last Queued Seed" : this.lastSeed}`;
+ break;
+ }
+ }
+
+}
+
+function showSeedBehaviorMenu(value, options, e, menu, node) {
+ const behaviorOptions = [
+ {
+ content: "🎲 Randomize",
+ callback: () => {
+ node.seedControl.setBehavior('randomize');
+ }
+ },
+ {
+ content: "➕ Increment",
+ callback: () => {
+ node.seedControl.setBehavior('increment');
+ }
+ },
+ {
+ content: "➖ Decrement",
+ callback: () => {
+ node.seedControl.setBehavior('decrement');
+ }
+ }
+ ];
+
+ new LiteGraph.ContextMenu(behaviorOptions, {
+ event: e,
+ callback: null,
+ parentMenu: menu,
+ node: node
+ });
+
+ return false; // This ensures the original context menu doesn't proceed
+}
+
+// Extension Definition
+app.registerExtension({
+ name: "efficiency.seedcontrol",
+ async beforeRegisterNodeDef(nodeType, nodeData, _app) {
+ if (NODE_WIDGET_MAP[nodeData.name]) {
+ addMenuHandler(nodeType, function (insertOption) {
+ // Check conditions before showing the seed behavior option
+ let showSeedOption = true;
+
+ if (nodeData.name === "Noise Control Script") {
+ // Check for 'add_seed_noise' widget being false
+ const addSeedNoiseWidget = this.widgets.find(w => w.name === 'add_seed_noise');
+ if (addSeedNoiseWidget && !addSeedNoiseWidget.value) {
+ showSeedOption = false;
+ }
+ } else if (nodeData.name === "HighRes-Fix Script") {
+ // Check for 'use_same_seed' widget being true
+ const useSameSeedWidget = this.widgets.find(w => w.name === 'use_same_seed');
+ if (useSameSeedWidget && useSameSeedWidget.value) {
+ showSeedOption = false;
+ }
+ }
+
+ if (showSeedOption) {
+ insertOption({
+ content: "🌱 Seed behavior...",
+ has_submenu: true,
+ callback: showSeedBehaviorMenu
+ });
+ }
+ });
+
+ const onNodeCreated = nodeType.prototype.onNodeCreated;
+ nodeType.prototype.onNodeCreated = function () {
+ onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
+ this.seedControl = new SeedControl(this, NODE_WIDGET_MAP[nodeData.name]);
+ this.seedControl.seedWidget.value = -1;
+ };
+ }
+ },
+});
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/widgethider.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/widgethider.js
new file mode 100644
index 0000000000000000000000000000000000000000..25878aa2316aab056d0a7c94a30318d3475ded1f
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/widgethider.js
@@ -0,0 +1,596 @@
+import { app } from "../../scripts/app.js";
+
+let origProps = {};
+let initialized = false;
+
+const findWidgetByName = (node, name) => {
+ return node.widgets ? node.widgets.find((w) => w.name === name) : null;
+};
+
+const doesInputWithNameExist = (node, name) => {
+ return node.inputs ? node.inputs.some((input) => input.name === name) : false;
+};
+
+const HIDDEN_TAG = "tschide";
+// Toggle Widget + change size
+function toggleWidget(node, widget, show = false, suffix = "") {
+ if (!widget || doesInputWithNameExist(node, widget.name)) return;
+
+ // Store the original properties of the widget if not already stored
+ if (!origProps[widget.name]) {
+ origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize };
+ }
+
+ const origSize = node.size;
+
+ // Set the widget type and computeSize based on the show flag
+ widget.type = show ? origProps[widget.name].origType : HIDDEN_TAG + suffix;
+ widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4];
+
+ // Recursively handle linked widgets if they exist
+ widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show));
+
+ // Calculate the new height for the node based on its computeSize method
+ const newHeight = node.computeSize()[1];
+ node.setSize([node.size[0], newHeight]);
+}
+
+const WIDGET_HEIGHT = 24;
+// Use for Multiline Widget Nodes (aka Efficient Loaders)
+function toggleWidget_2(node, widget, show = false, suffix = "") {
+ if (!widget || doesInputWithNameExist(node, widget.name)) return;
+
+ const isCurrentlyVisible = widget.type !== HIDDEN_TAG + suffix;
+ if (isCurrentlyVisible === show) return; // Early exit if widget is already in the desired state
+
+ if (!origProps[widget.name]) {
+ origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize };
+ }
+
+ widget.type = show ? origProps[widget.name].origType : HIDDEN_TAG + suffix;
+ widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4];
+
+ if (initialized){
+ const adjustment = show ? WIDGET_HEIGHT : -WIDGET_HEIGHT;
+ node.setSize([node.size[0], node.size[1] + adjustment]);
+ }
+}
+
+// New function to handle widget visibility based on input_mode
+function handleInputModeWidgetsVisibility(node, inputModeValue) {
+ // Utility function to generate widget names up to a certain count
+ function generateWidgetNames(baseName, count) {
+ return Array.from({ length: count }, (_, i) => `${baseName}_${i + 1}`);
+ }
+
+ // Common widget groups
+ const batchWidgets = ["batch_path", "subdirectories", "batch_sort", "batch_max"];
+ const xbatchWidgets = ["X_batch_path", "X_subdirectories", "X_batch_sort", "X_batch_max"];
+ const ckptWidgets = [...generateWidgetNames("ckpt_name", 50)];
+ const clipSkipWidgets = [...generateWidgetNames("clip_skip", 50)];
+ const vaeNameWidgets = [...generateWidgetNames("vae_name", 50)];
+ const loraNameWidgets = [...generateWidgetNames("lora_name", 50)];
+ const loraWtWidgets = [...generateWidgetNames("lora_wt", 50)];
+ const modelStrWidgets = [...generateWidgetNames("model_str", 50)];
+ const clipStrWidgets = [...generateWidgetNames("clip_str", 50)];
+ const xWidgets = ["X_batch_count", "X_first_value", "X_last_value"]
+ const yWidgets = ["Y_batch_count", "Y_first_value", "Y_last_value"]
+
+ const nodeVisibilityMap = {
+ "LoRA Stacker": {
+ "simple": [...modelStrWidgets, ...clipStrWidgets],
+ "advanced": [...loraWtWidgets]
+ },
+ "XY Input: Steps": {
+ "steps": ["first_start_step", "last_start_step", "first_end_step", "last_end_step", "first_refine_step", "last_refine_step"],
+ "start_at_step": ["first_step", "last_step", "first_end_step", "last_end_step", "first_refine_step", "last_refine_step"],
+ "end_at_step": ["first_step", "last_step", "first_start_step", "last_start_step", "first_refine_step", "last_refine_step"],
+ "refine_at_step": ["first_step", "last_step", "first_start_step", "last_start_step", "first_end_step", "last_end_step"]
+ },
+ "XY Input: VAE": {
+ "VAE Names": [...batchWidgets],
+ "VAE Batch": [...vaeNameWidgets, "vae_count"]
+ },
+ "XY Input: Checkpoint": {
+ "Ckpt Names": [...clipSkipWidgets, ...vaeNameWidgets, ...batchWidgets],
+ "Ckpt Names+ClipSkip": [...vaeNameWidgets, ...batchWidgets],
+ "Ckpt Names+ClipSkip+VAE": [...batchWidgets],
+ "Checkpoint Batch": [...ckptWidgets, ...clipSkipWidgets, ...vaeNameWidgets, "ckpt_count"]
+ },
+ "XY Input: LoRA": {
+ "LoRA Names": [...modelStrWidgets, ...clipStrWidgets, ...batchWidgets],
+ "LoRA Names+Weights": [...batchWidgets, "model_strength", "clip_strength"],
+ "LoRA Batch": [...loraNameWidgets, ...modelStrWidgets, ...clipStrWidgets, "lora_count"]
+ },
+ "XY Input: LoRA Plot": {
+ "X: LoRA Batch, Y: LoRA Weight": ["lora_name", "model_strength", "clip_strength", "X_first_value", "X_last_value"],
+ "X: LoRA Batch, Y: Model Strength": ["lora_name", "model_strength", "model_strength", "X_first_value", "X_last_value"],
+ "X: LoRA Batch, Y: Clip Strength": ["lora_name", "clip_strength", "X_first_value", "X_last_value"],
+ "X: Model Strength, Y: Clip Strength": [...xbatchWidgets, "model_strength", "clip_strength"],
+ },
+ "XY Input: Control Net": {
+ "strength": ["first_start_percent", "last_start_percent", "first_end_percent", "last_end_percent", "strength"],
+ "start_percent": ["first_strength", "last_strength", "first_end_percent", "last_end_percent", "start_percent"],
+ "end_percent": ["first_strength", "last_strength", "first_start_percent", "last_start_percent", "end_percent"]
+ },
+ "XY Input: Control Net Plot": {
+ "X: Strength, Y: Start%": ["strength", "start_percent"],
+ "X: Strength, Y: End%": ["strength","end_percent"],
+ "X: Start%, Y: Strength": ["start_percent", "strength"],
+ "X: Start%, Y: End%": ["start_percent", "end_percent"],
+ "X: End%, Y: Strength": ["end_percent", "strength"],
+ "X: End%, Y: Start%": ["end_percent", "start_percent"],
+ }
+ };
+
+ const inputModeVisibilityMap = nodeVisibilityMap[node.comfyClass];
+
+ if (!inputModeVisibilityMap || !inputModeVisibilityMap[inputModeValue]) return;
+
+ // Reset all widgets to visible
+ for (const key in inputModeVisibilityMap) {
+ for (const widgetName of inputModeVisibilityMap[key]) {
+ const widget = findWidgetByName(node, widgetName);
+ toggleWidget(node, widget, true);
+ }
+ }
+
+ // Hide the specific widgets for the current input_mode value
+ for (const widgetName of inputModeVisibilityMap[inputModeValue]) {
+ const widget = findWidgetByName(node, widgetName);
+ toggleWidget(node, widget, false);
+ }
+}
+
+// Handle multi-widget visibilities
+function handleVisibility(node, countValue, node_type) {
+ const inputModeValue = findWidgetByName(node, "input_mode").value;
+ const baseNamesMap = {
+ "LoRA": ["lora_name", "model_str", "clip_str"],
+ "Checkpoint": ["ckpt_name", "clip_skip", "vae_name"],
+ "LoRA Stacker": ["lora_name", "model_str", "clip_str", "lora_wt"]
+ };
+
+ const baseNames = baseNamesMap[node_type];
+
+ const isBatchMode = inputModeValue.includes("Batch");
+ if (isBatchMode) {countValue = 0;}
+
+ for (let i = 1; i <= 50; i++) {
+ const nameWidget = findWidgetByName(node, `${baseNames[0]}_${i}`);
+ const firstWidget = findWidgetByName(node, `${baseNames[1]}_${i}`);
+ const secondWidget = findWidgetByName(node, `${baseNames[2]}_${i}`);
+ const thirdWidget = node_type === "LoRA Stacker" ? findWidgetByName(node, `${baseNames[3]}_${i}`) : null;
+
+ if (i <= countValue) {
+ toggleWidget(node, nameWidget, true);
+
+ if (node_type === "LoRA Stacker") {
+ if (inputModeValue === "simple") {
+ toggleWidget(node, firstWidget, false); // model_str
+ toggleWidget(node, secondWidget, false); // clip_str
+ toggleWidget(node, thirdWidget, true); // lora_wt
+ } else if (inputModeValue === "advanced") {
+ toggleWidget(node, firstWidget, true); // model_str
+ toggleWidget(node, secondWidget, true); // clip_str
+ toggleWidget(node, thirdWidget, false); // lora_wt
+ }
+ } else if (node_type === "Checkpoint") {
+ if (inputModeValue.includes("ClipSkip")){toggleWidget(node, firstWidget, true);}
+ if (inputModeValue.includes("VAE")){toggleWidget(node, secondWidget, true);}
+ } else if (node_type === "LoRA") {
+ if (inputModeValue.includes("Weights")){
+ toggleWidget(node, firstWidget, true);
+ toggleWidget(node, secondWidget, true);
+ }
+ }
+ }
+ else {
+ toggleWidget(node, nameWidget, false);
+ toggleWidget(node, firstWidget, false);
+ toggleWidget(node, secondWidget, false);
+ if (thirdWidget) {toggleWidget(node, thirdWidget, false);}
+ }
+ }
+}
+
+// Sampler & Scheduler XY input visibility logic
+function handleSamplerSchedulerVisibility(node, countValue, targetParameter) {
+ for (let i = 1; i <= 50; i++) {
+ const samplerWidget = findWidgetByName(node, `sampler_${i}`);
+ const schedulerWidget = findWidgetByName(node, `scheduler_${i}`);
+
+ if (i <= countValue) {
+ if (targetParameter === "sampler") {
+ toggleWidget(node, samplerWidget, true);
+ toggleWidget(node, schedulerWidget, false);
+ } else if (targetParameter === "scheduler") {
+ toggleWidget(node, samplerWidget, false);
+ toggleWidget(node, schedulerWidget, true);
+ } else { // targetParameter is "sampler & scheduler"
+ toggleWidget(node, samplerWidget, true);
+ toggleWidget(node, schedulerWidget, true);
+ }
+ } else {
+ toggleWidget(node, samplerWidget, false);
+ toggleWidget(node, schedulerWidget, false);
+ }
+ }
+}
+
+// Handle simple widget visibility based on a count
+function handleWidgetVisibility(node, thresholdValue, widgetNamePrefix, maxCount) {
+ for (let i = 1; i <= maxCount; i++) {
+ const widget = findWidgetByName(node, `${widgetNamePrefix}${i}`);
+ if (widget) {
+ toggleWidget(node, widget, i <= thresholdValue);
+ }
+ }
+}
+
+// Disable the 'Ckpt Name+ClipSkip+VAE' option if 'target_ckpt' is "Refiner"
+let last_ckpt_input_mode;
+let last_target_ckpt;
+function xyCkptRefinerOptionsRemove(widget, node) {
+
+ let target_ckpt = findWidgetByName(node, "target_ckpt").value
+ let input_mode = widget.value
+
+ if ((input_mode === "Ckpt Names+ClipSkip+VAE") && (target_ckpt === "Refiner")) {
+ if (last_ckpt_input_mode === "Ckpt Names+ClipSkip") {
+ if (last_target_ckpt === "Refiner"){
+ widget.value = "Checkpoint Batch";
+ } else {widget.value = "Ckpt Names+ClipSkip";}
+ } else if (last_ckpt_input_mode === "Checkpoint Batch") {
+ if (last_target_ckpt === "Refiner"){
+ widget.value = "Ckpt Names+ClipSkip";
+ } else {widget.value = "Checkpoint Batch";}
+ } else if (last_ckpt_input_mode !== 'undefined') {
+ widget.value = last_ckpt_input_mode;
+ } else {
+ widget.value = "Ckpt Names";
+ }
+ } else if (input_mode !== "Ckpt Names+ClipSkip+VAE"){
+ last_ckpt_input_mode = input_mode;
+ }
+ last_target_ckpt = target_ckpt
+}
+
+// Create a map of node titles to their respective widget handlers
+const nodeWidgetHandlers = {
+ "Efficient Loader": {
+ 'lora_name': handleEfficientLoaderLoraName
+ },
+ "Eff. Loader SDXL": {
+ 'refiner_ckpt_name': handleEffLoaderSDXLRefinerCkptName
+ },
+ "LoRA Stacker": {
+ 'input_mode': handleLoRAStackerInputMode,
+ 'lora_count': handleLoRAStackerLoraCount
+ },
+ "XY Input: Steps": {
+ 'target_parameter': handleXYInputStepsTargetParameter
+ },
+ "XY Input: Sampler/Scheduler": {
+ 'target_parameter': handleXYInputSamplerSchedulerTargetParameter,
+ 'input_count': handleXYInputSamplerSchedulerInputCount
+ },
+ "XY Input: VAE": {
+ 'input_mode': handleXYInputVAEInputMode,
+ 'vae_count': handleXYInputVAEVaeCount
+ },
+ "XY Input: Prompt S/R": {
+ 'replace_count': handleXYInputPromptSRReplaceCount
+ },
+ "XY Input: Checkpoint": {
+ 'input_mode': handleXYInputCheckpointInputMode,
+ 'ckpt_count': handleXYInputCheckpointCkptCount,
+ 'target_ckpt': handleXYInputCheckpointTargetCkpt
+ },
+ "XY Input: LoRA": {
+ 'input_mode': handleXYInputLoRAInputMode,
+ 'lora_count': handleXYInputLoRALoraCount
+ },
+ "XY Input: LoRA Plot": {
+ 'input_mode': handleXYInputLoRAPlotInputMode
+ },
+ "XY Input: LoRA Stacks": {
+ 'node_state': handleXYInputLoRAStacksNodeState
+ },
+ "XY Input: Control Net": {
+ 'target_parameter': handleXYInputControlNetTargetParameter
+ },
+ "XY Input: Control Net Plot": {
+ 'plot_type': handleXYInputControlNetPlotPlotType
+ },
+ "Noise Control Script": {
+ 'add_seed_noise': handleNoiseControlScript
+ },
+ "HighRes-Fix Script": {
+ 'upscale_type': handleHiResFixScript,
+ 'use_same_seed': handleHiResFixScript,
+ 'use_controlnet':handleHiResFixScript
+ },
+ "Tiled Upscaler Script": {
+ 'use_controlnet':handleTiledUpscalerScript
+ },
+};
+
+// In the main function where widgetLogic is called
+function widgetLogic(node, widget) {
+ // Retrieve the handler for the current node title and widget name
+ const handler = nodeWidgetHandlers[node.comfyClass]?.[widget.name];
+ if (handler) {
+ handler(node, widget);
+ }
+}
+
+// Efficient Loader Handlers
+function handleEfficientLoaderLoraName(node, widget) {
+ if (widget.value === 'None') {
+ toggleWidget_2(node, findWidgetByName(node, 'lora_model_strength'));
+ toggleWidget_2(node, findWidgetByName(node, 'lora_clip_strength'));
+ } else {
+ toggleWidget_2(node, findWidgetByName(node, 'lora_model_strength'), true);
+ toggleWidget_2(node, findWidgetByName(node, 'lora_clip_strength'), true);
+ }
+}
+
+// Eff. Loader SDXL Handlers
+function handleEffLoaderSDXLRefinerCkptName(node, widget) {
+ if (widget.value === 'None') {
+ toggleWidget_2(node, findWidgetByName(node, 'refiner_clip_skip'));
+ toggleWidget_2(node, findWidgetByName(node, 'positive_ascore'));
+ toggleWidget_2(node, findWidgetByName(node, 'negative_ascore'));
+ } else {
+ toggleWidget_2(node, findWidgetByName(node, 'refiner_clip_skip'), true);
+ toggleWidget_2(node, findWidgetByName(node, 'positive_ascore'), true);
+ toggleWidget_2(node, findWidgetByName(node, 'negative_ascore'), true);
+ }
+}
+
+// Noise Control Script Seed Handler
+function handleNoiseControlScript(node, widget) {
+
+ function ensureSeedControlExists(callback) {
+ if (node.seedControl && node.seedControl.lastSeedButton) {
+ callback();
+ } else {
+ setTimeout(() => ensureSeedControlExists(callback), 0);
+ }
+ }
+
+ ensureSeedControlExists(() => {
+ if (widget.value === false) {
+ toggleWidget(node, findWidgetByName(node, 'seed'));
+ toggleWidget(node, findWidgetByName(node, 'weight'));
+ toggleWidget(node, node.seedControl.lastSeedButton);
+ node.seedControl.lastSeedButton.disabled = true; // Disable the button
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'seed'), true);
+ toggleWidget(node, findWidgetByName(node, 'weight'), true);
+ node.seedControl.lastSeedButton.disabled = false; // Enable the button
+ toggleWidget(node, node.seedControl.lastSeedButton, true);
+ }
+ });
+
+}
+
+/// HighRes-Fix Script Handlers
+function handleHiResFixScript(node, widget) {
+
+ function ensureSeedControlExists(callback) {
+ if (node.seedControl && node.seedControl.lastSeedButton) {
+ callback();
+ } else {
+ setTimeout(() => ensureSeedControlExists(callback), 0);
+ }
+ }
+
+ if (findWidgetByName(node, 'upscale_type').value === "latent") {
+ toggleWidget(node, findWidgetByName(node, 'pixel_upscaler'));
+
+ toggleWidget(node, findWidgetByName(node, 'hires_ckpt_name'), true);
+ toggleWidget(node, findWidgetByName(node, 'latent_upscaler'), true);
+ toggleWidget(node, findWidgetByName(node, 'use_same_seed'), true);
+ toggleWidget(node, findWidgetByName(node, 'hires_steps'), true);
+ toggleWidget(node, findWidgetByName(node, 'denoise'), true);
+ toggleWidget(node, findWidgetByName(node, 'iterations'), true);
+
+ ensureSeedControlExists(() => {
+ if (findWidgetByName(node, 'use_same_seed').value == true) {
+ toggleWidget(node, findWidgetByName(node, 'seed'));
+ toggleWidget(node, node.seedControl.lastSeedButton);
+ node.seedControl.lastSeedButton.disabled = true; // Disable the button
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'seed'), true);
+ node.seedControl.lastSeedButton.disabled = false; // Enable the button
+ toggleWidget(node, node.seedControl.lastSeedButton, true);
+ }
+ });
+
+ if (findWidgetByName(node, 'use_controlnet').value == '_'){
+ toggleWidget(node, findWidgetByName(node, 'use_controlnet'));
+ toggleWidget(node, findWidgetByName(node, 'control_net_name'));
+ toggleWidget(node, findWidgetByName(node, 'strength'));
+ toggleWidget(node, findWidgetByName(node, 'preprocessor'));
+ toggleWidget(node, findWidgetByName(node, 'preprocessor_imgs'));
+ }
+ else{
+ toggleWidget(node, findWidgetByName(node, 'use_controlnet'), true);
+
+ if (findWidgetByName(node, 'use_controlnet').value == true){
+ toggleWidget(node, findWidgetByName(node, 'control_net_name'), true);
+ toggleWidget(node, findWidgetByName(node, 'strength'), true);
+ toggleWidget(node, findWidgetByName(node, 'preprocessor'), true);
+ toggleWidget(node, findWidgetByName(node, 'preprocessor_imgs'), true);
+ }
+ else{
+ toggleWidget(node, findWidgetByName(node, 'control_net_name'));
+ toggleWidget(node, findWidgetByName(node, 'strength'));
+ toggleWidget(node, findWidgetByName(node, 'preprocessor'));
+ toggleWidget(node, findWidgetByName(node, 'preprocessor_imgs'));
+ }
+ }
+
+ } else if (findWidgetByName(node, 'upscale_type').value === "pixel") {
+ toggleWidget(node, findWidgetByName(node, 'hires_ckpt_name'));
+ toggleWidget(node, findWidgetByName(node, 'latent_upscaler'));
+ toggleWidget(node, findWidgetByName(node, 'use_same_seed'));
+ toggleWidget(node, findWidgetByName(node, 'hires_steps'));
+ toggleWidget(node, findWidgetByName(node, 'denoise'));
+ toggleWidget(node, findWidgetByName(node, 'iterations'));
+ toggleWidget(node, findWidgetByName(node, 'seed'));
+ ensureSeedControlExists(() => {
+ toggleWidget(node, node.seedControl.lastSeedButton);
+ node.seedControl.lastSeedButton.disabled = true; // Disable the button
+ });
+ toggleWidget(node, findWidgetByName(node, 'use_controlnet'));
+ toggleWidget(node, findWidgetByName(node, 'control_net_name'));
+ toggleWidget(node, findWidgetByName(node, 'strength'));
+ toggleWidget(node, findWidgetByName(node, 'preprocessor'));
+ toggleWidget(node, findWidgetByName(node, 'preprocessor_imgs'));
+
+ toggleWidget(node, findWidgetByName(node, 'pixel_upscaler'), true);
+ }
+}
+
+/// Tiled Upscaler Script Handler
+function handleTiledUpscalerScript(node, widget) {
+ if (findWidgetByName(node, 'use_controlnet').value == true){
+ toggleWidget(node, findWidgetByName(node, 'tile_controlnet'), true);
+ toggleWidget(node, findWidgetByName(node, 'strength'), true);
+ }
+ else{
+ toggleWidget(node, findWidgetByName(node, 'tile_controlnet'));
+ toggleWidget(node, findWidgetByName(node, 'strength'));
+ }
+}
+
+// LoRA Stacker Handlers
+function handleLoRAStackerInputMode(node, widget) {
+ handleInputModeWidgetsVisibility(node, widget.value);
+ handleVisibility(node, findWidgetByName(node, "lora_count").value, "LoRA Stacker");
+}
+
+function handleLoRAStackerLoraCount(node, widget) {
+ handleVisibility(node, widget.value, "LoRA Stacker");
+}
+
+// XY Input: Steps Handlers
+function handleXYInputStepsTargetParameter(node, widget) {
+ handleInputModeWidgetsVisibility(node, widget.value);
+}
+
+// XY Input: Sampler/Scheduler Handlers
+function handleXYInputSamplerSchedulerTargetParameter(node, widget) {
+ handleSamplerSchedulerVisibility(node, findWidgetByName(node, 'input_count').value, widget.value);
+}
+
+function handleXYInputSamplerSchedulerInputCount(node, widget) {
+ handleSamplerSchedulerVisibility(node, widget.value, findWidgetByName(node, 'target_parameter').value);
+}
+
+// XY Input: VAE Handlers
+function handleXYInputVAEInputMode(node, widget) {
+ handleInputModeWidgetsVisibility(node, widget.value);
+ if (widget.value === "VAE Names") {
+ handleWidgetVisibility(node, findWidgetByName(node, "vae_count").value, "vae_name_", 50);
+ } else {
+ handleWidgetVisibility(node, 0, "vae_name_", 50);
+ }
+}
+
+function handleXYInputVAEVaeCount(node, widget) {
+ if (findWidgetByName(node, "input_mode").value === "VAE Names") {
+ handleWidgetVisibility(node, widget.value, "vae_name_", 50);
+ }
+}
+
+// XY Input: Prompt S/R Handlers
+function handleXYInputPromptSRReplaceCount(node, widget) {
+ handleWidgetVisibility(node, widget.value, "replace_", 49);
+}
+
+// XY Input: Checkpoint Handlers
+function handleXYInputCheckpointInputMode(node, widget) {
+ xyCkptRefinerOptionsRemove(widget, node);
+ handleInputModeWidgetsVisibility(node, widget.value);
+ handleVisibility(node, findWidgetByName(node, "ckpt_count").value, "Checkpoint");
+}
+
+function handleXYInputCheckpointCkptCount(node, widget) {
+ handleVisibility(node, widget.value, "Checkpoint");
+}
+
+function handleXYInputCheckpointTargetCkpt(node, widget) {
+ xyCkptRefinerOptionsRemove(findWidgetByName(node, "input_mode"), node);
+}
+
+// XY Input: LoRA Handlers
+function handleXYInputLoRAInputMode(node, widget) {
+ handleInputModeWidgetsVisibility(node, widget.value);
+ handleVisibility(node, findWidgetByName(node, "lora_count").value, "LoRA");
+}
+
+function handleXYInputLoRALoraCount(node, widget) {
+ handleVisibility(node, widget.value, "LoRA");
+}
+
+// XY Input: LoRA Plot Handlers
+function handleXYInputLoRAPlotInputMode(node, widget) {
+ handleInputModeWidgetsVisibility(node, widget.value);
+}
+
+// XY Input: LoRA Stacks Handlers
+function handleXYInputLoRAStacksNodeState(node, widget) {
+ toggleWidget(node, findWidgetByName(node, "node_state"), false);
+}
+
+// XY Input: Control Net Handlers
+function handleXYInputControlNetTargetParameter(node, widget) {
+ handleInputModeWidgetsVisibility(node, widget.value);
+}
+
+// XY Input: Control Net Plot Handlers
+function handleXYInputControlNetPlotPlotType(node, widget) {
+ handleInputModeWidgetsVisibility(node, widget.value);
+}
+
+app.registerExtension({
+ name: "efficiency.widgethider",
+ nodeCreated(node) {
+ for (const w of node.widgets || []) {
+ let widgetValue = w.value;
+
+ // Store the original descriptor if it exists
+ let originalDescriptor = Object.getOwnPropertyDescriptor(w, 'value');
+
+ widgetLogic(node, w);
+
+ Object.defineProperty(w, 'value', {
+ get() {
+ // If there's an original getter, use it. Otherwise, return widgetValue.
+ let valueToReturn = originalDescriptor && originalDescriptor.get
+ ? originalDescriptor.get.call(w)
+ : widgetValue;
+
+ return valueToReturn;
+ },
+ set(newVal) {
+
+ // If there's an original setter, use it. Otherwise, set widgetValue.
+ if (originalDescriptor && originalDescriptor.set) {
+ originalDescriptor.set.call(w, newVal);
+ } else {
+ widgetValue = newVal;
+ }
+
+ widgetLogic(node, w);
+ }
+ });
+ }
+ setTimeout(() => {initialized = true;}, 500);
+ }
+});
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/workflowfix.js b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/workflowfix.js
new file mode 100644
index 0000000000000000000000000000000000000000..82c51421caccd7caca517dce32447ba9f90c2833
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/js/workflowfix.js
@@ -0,0 +1,142 @@
+// Detect and update Efficiency Nodes from v1.92 to v2.00 changes (Final update?)
+import { app } from '../../scripts/app.js'
+import { addNode } from "./node_options/common/utils.js";
+
+const ext = {
+ name: "efficiency.WorkflowFix",
+};
+
+function reloadHiResFixNode(originalNode) {
+
+ // Safeguard against missing 'pos' property
+ const position = originalNode.pos && originalNode.pos.length === 2 ? { x: originalNode.pos[0], y: originalNode.pos[1] } : { x: 0, y: 0 };
+
+ // Recreate the node
+ const newNode = addNode("HighRes-Fix Script", originalNode, position);
+
+ // Transfer input connections from old node to new node
+ originalNode.inputs.forEach((input, index) => {
+ if (input && input.link !== null) {
+ const originLinkInfo = originalNode.graph.links[input.link];
+ if (originLinkInfo) {
+ const originNode = originalNode.graph.getNodeById(originLinkInfo.origin_id);
+ if (originNode) {
+ originNode.connect(originLinkInfo.origin_slot, newNode, index);
+ }
+ }
+ }
+ });
+
+ // Transfer output connections from old node to new node
+ originalNode.outputs.forEach((output, index) => {
+ if (output && output.links) {
+ output.links.forEach(link => {
+ const targetLinkInfo = originalNode.graph.links[link];
+ if (targetLinkInfo) {
+ const targetNode = originalNode.graph.getNodeById(targetLinkInfo.target_id);
+ if (targetNode) {
+ newNode.connect(index, targetNode, targetLinkInfo.target_slot);
+ }
+ }
+ });
+ }
+ });
+
+ // Remove the original node after all connections are transferred
+ originalNode.graph.remove(originalNode);
+
+ return newNode;
+}
+
+ext.loadedGraphNode = function(node, app) {
+ const originalNode = node; // This line ensures that originalNode refers to the provided node
+ const kSamplerTypes = [
+ "KSampler (Efficient)",
+ "KSampler Adv. (Efficient)",
+ "KSampler SDXL (Eff.)"
+ ];
+
+ // EFFICIENT LOADER & EFF. LOADER SDXL
+ /* Changes:
+ Added "token_normalization" & "weight_interpretation" widget below prompt text boxes,
+ below code fixes the widget values for empty_latent_width, empty_latent_height, and batch_size
+ by shifting down by 2 widget values starting from the "token_normalization" widget.
+ Logic triggers when "token_normalization" is a number instead of a string.
+ */
+ if (node.comfyClass === "Efficient Loader" || node.comfyClass === "Eff. Loader SDXL") {
+ const tokenWidget = node.widgets.find(w => w.name === "token_normalization");
+ const weightWidget = node.widgets.find(w => w.name === "weight_interpretation");
+
+ if (typeof tokenWidget.value === 'number') {
+ console.log("[EfficiencyUpdate]", `Fixing '${node.comfyClass}' token and weight widgets:`, node);
+ const index = node.widgets.indexOf(tokenWidget);
+ if (index !== -1) {
+ for (let i = node.widgets.length - 1; i > index + 1; i--) {
+ node.widgets[i].value = node.widgets[i - 2].value;
+ }
+ }
+ tokenWidget.value = "none";
+ weightWidget.value = "comfy";
+ }
+ }
+
+ // KSAMPLER (EFFICIENT), KSAMPLER ADV. (EFFICIENT), & KSAMPLER SDXL (EFF.)
+ /* Changes:
+ Removed the "sampler_state" widget which cause all widget values to shift down by a factor of 1.
+ Fix involves moving all widget values by -1. "vae_decode" value is lost in this process, so in
+ below fix I manually set it to its default value of "true".
+ */
+ else if (kSamplerTypes.includes(node.comfyClass)) {
+
+ const seedWidgetName = (node.comfyClass === "KSampler (Efficient)") ? "seed" : "noise_seed";
+ const stepsWidgetName = (node.comfyClass === "KSampler (Efficient)") ? "steps" : "start_at_step";
+
+ const seedWidget = node.widgets.find(w => w.name === seedWidgetName);
+ const stepsWidget = node.widgets.find(w => w.name === stepsWidgetName);
+
+ if (isNaN(seedWidget.value) && isNaN(stepsWidget.value)) {
+ console.log("[EfficiencyUpdate]", `Fixing '${node.comfyClass}' node widgets:`, node);
+ for (let i = 0; i < node.widgets.length - 1; i++) {
+ node.widgets[i].value = node.widgets[i + 1].value;
+ }
+ node.widgets[node.widgets.length - 1].value = "true";
+ }
+ }
+
+ // HIGHRES-FIX SCRIPT
+ /* Changes:
+ Many new changes where added, so in order to properly update, aquired the values of the original
+ widgets, reload a new node, transffer the known original values, and transffer connection.
+ This fix is triggered when the upscale_type widget is neither "latent" or "pixel".
+ */
+ // Check if the current node is "HighRes-Fix Script" and if any of the above fixes were applied
+ else if (node.comfyClass === "HighRes-Fix Script") {
+ const upscaleTypeWidget = node.widgets.find(w => w.name === "upscale_type");
+
+ if (upscaleTypeWidget && upscaleTypeWidget.value !== "latent" && upscaleTypeWidget.value !== "pixel") {
+ console.log("[EfficiencyUpdate]", "Reloading 'HighRes-Fix Script' node:", node);
+
+ // Reload the node and get the new node instance
+ const newNode = reloadHiResFixNode(node);
+
+ // Update the widgets of the new node
+ const targetWidgetNames = ["latent_upscaler", "upscale_by", "hires_steps", "denoise", "iterations"];
+
+ // Extract the first five values of the original node
+ const originalValues = originalNode.widgets.slice(0, 5).map(w => w.value);
+
+ targetWidgetNames.forEach((name, index) => {
+ const widget = newNode.widgets.find(w => w.name === name);
+ if (widget && originalValues[index] !== undefined) {
+ if (name === "latent_upscaler" && typeof originalValues[index] === 'string') {
+ widget.value = originalValues[index].replace("SD-Latent-Upscaler", "city96");
+ } else {
+ widget.value = originalValues[index];
+ }
+ }
+ });
+ }
+ }
+}
+
+app.registerExtension(ext);
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/models/readme.md b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/models/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/models/readme.md
@@ -0,0 +1 @@
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/node_settings.json b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/node_settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..66b7f9e2ae35a5b43adc1ddff521f07d3884f5c9
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/node_settings.json
@@ -0,0 +1,19 @@
+{
+ "Efficient Loader": {
+ "model_cache": {
+ "vae": 1,
+ "ckpt": 1,
+ "lora": 1,
+ "refn": 1
+ }
+ },
+ "XY Plot": {
+ "model_cache": {
+ "vae": 5,
+ "ckpt": 5,
+ "lora": 5,
+ "refn": 1
+ }
+ }
+}
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__init__.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4287ca8617970fa8fc025b75cb319c7032706910
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__init__.py
@@ -0,0 +1 @@
+#
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/__init__.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..46205039b3a35c4e4d46728865395ec390334386
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/__init__.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/bnk_adv_encode.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/bnk_adv_encode.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..74830bf1eaf2d3dc8f0c8a985c73597031850ada
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/bnk_adv_encode.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/bnk_tiled_samplers.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/bnk_tiled_samplers.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c6d6bf4781e76a9e7cfb7f047f7caf51ce6fa853
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/bnk_tiled_samplers.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/bnk_tiling.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/bnk_tiling.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..18e73a29e2b75d3874e3c181dfa5274d090b5afd
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/bnk_tiling.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/cg_mixed_seed_noise.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/cg_mixed_seed_noise.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..17368b5a55c9fd0ffdb78083d430ab485ed8ec7d
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/cg_mixed_seed_noise.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/city96_latent_upscaler.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/city96_latent_upscaler.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4883a72d9fcb954753cbce6ec5e156a3d3ff1921
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/city96_latent_upscaler.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/smZ_cfg_denoiser.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/smZ_cfg_denoiser.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..50fda220340ca6ad500486d26d3c16fef4822d55
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/smZ_cfg_denoiser.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/smZ_rng_source.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/smZ_rng_source.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0025c3099dea516dbd415f26d404759db33d8176
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/smZ_rng_source.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/ttl_nn_latent_upscaler.cpython-310.pyc b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/ttl_nn_latent_upscaler.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..51412ce286c61e5831aa0b1eff3862562c5eab85
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/__pycache__/ttl_nn_latent_upscaler.cpython-310.pyc differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/bnk_adv_encode.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/bnk_adv_encode.py
new file mode 100644
index 0000000000000000000000000000000000000000..d095746cb553653974272e2e96cb68b275723ff5
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/bnk_adv_encode.py
@@ -0,0 +1,341 @@
+import torch
+import numpy as np
+import itertools
+from math import gcd
+
+from comfy import model_management
+from comfy.sdxl_clip import SDXLClipModel, SDXLRefinerClipModel, SDXLClipG
+
+def _grouper(n, iterable):
+ it = iter(iterable)
+ while True:
+ chunk = list(itertools.islice(it, n))
+ if not chunk:
+ return
+ yield chunk
+
+def _norm_mag(w, n):
+ d = w - 1
+ return 1 + np.sign(d) * np.sqrt(np.abs(d)**2 / n)
+ #return np.sign(w) * np.sqrt(np.abs(w)**2 / n)
+
+def divide_length(word_ids, weights):
+ sums = dict(zip(*np.unique(word_ids, return_counts=True)))
+ sums[0] = 1
+ weights = [[_norm_mag(w, sums[id]) if id != 0 else 1.0
+ for w, id in zip(x, y)] for x, y in zip(weights, word_ids)]
+ return weights
+
+def shift_mean_weight(word_ids, weights):
+ delta = 1 - np.mean([w for x, y in zip(weights, word_ids) for w, id in zip(x,y) if id != 0])
+ weights = [[w if id == 0 else w+delta
+ for w, id in zip(x, y)] for x, y in zip(weights, word_ids)]
+ return weights
+
+def scale_to_norm(weights, word_ids, w_max):
+ top = np.max(weights)
+ w_max = min(top, w_max)
+ weights = [[w_max if id == 0 else (w/top) * w_max
+ for w, id in zip(x, y)] for x, y in zip(weights, word_ids)]
+ return weights
+
+def from_zero(weights, base_emb):
+ weight_tensor = torch.tensor(weights, dtype=base_emb.dtype, device=base_emb.device)
+ weight_tensor = weight_tensor.reshape(1,-1,1).expand(base_emb.shape)
+ return base_emb * weight_tensor
+
+def mask_word_id(tokens, word_ids, target_id, mask_token):
+ new_tokens = [[mask_token if wid == target_id else t
+ for t, wid in zip(x,y)] for x,y in zip(tokens, word_ids)]
+ mask = np.array(word_ids) == target_id
+ return (new_tokens, mask)
+
+def batched_clip_encode(tokens, length, encode_func, num_chunks):
+ embs = []
+ for e in _grouper(32, tokens):
+ enc, pooled = encode_func(e)
+ enc = enc.reshape((len(e), length, -1))
+ embs.append(enc)
+ embs = torch.cat(embs)
+ embs = embs.reshape((len(tokens) // num_chunks, length * num_chunks, -1))
+ return embs
+
+def from_masked(tokens, weights, word_ids, base_emb, length, encode_func, m_token=266):
+ pooled_base = base_emb[0,length-1:length,:]
+ wids, inds = np.unique(np.array(word_ids).reshape(-1), return_index=True)
+ weight_dict = dict((id,w)
+ for id,w in zip(wids ,np.array(weights).reshape(-1)[inds])
+ if w != 1.0)
+
+ if len(weight_dict) == 0:
+ return torch.zeros_like(base_emb), base_emb[0,length-1:length,:]
+
+ weight_tensor = torch.tensor(weights, dtype=base_emb.dtype, device=base_emb.device)
+ weight_tensor = weight_tensor.reshape(1,-1,1).expand(base_emb.shape)
+
+ #m_token = (clip.tokenizer.end_token, 1.0) if clip.tokenizer.pad_with_end else (0,1.0)
+ #TODO: find most suitable masking token here
+ m_token = (m_token, 1.0)
+
+ ws = []
+ masked_tokens = []
+ masks = []
+
+ #create prompts
+ for id, w in weight_dict.items():
+ masked, m = mask_word_id(tokens, word_ids, id, m_token)
+ masked_tokens.extend(masked)
+
+ m = torch.tensor(m, dtype=base_emb.dtype, device=base_emb.device)
+ m = m.reshape(1,-1,1).expand(base_emb.shape)
+ masks.append(m)
+
+ ws.append(w)
+
+ #batch process prompts
+ embs = batched_clip_encode(masked_tokens, length, encode_func, len(tokens))
+ masks = torch.cat(masks)
+
+ embs = (base_emb.expand(embs.shape) - embs)
+ pooled = embs[0,length-1:length,:]
+
+ embs *= masks
+ embs = embs.sum(axis=0, keepdim=True)
+
+ pooled_start = pooled_base.expand(len(ws), -1)
+ ws = torch.tensor(ws).reshape(-1,1).expand(pooled_start.shape)
+ pooled = (pooled - pooled_start) * (ws - 1)
+ pooled = pooled.mean(axis=0, keepdim=True)
+
+ return ((weight_tensor - 1) * embs), pooled_base + pooled
+
+def mask_inds(tokens, inds, mask_token):
+ clip_len = len(tokens[0])
+ inds_set = set(inds)
+ new_tokens = [[mask_token if i*clip_len + j in inds_set else t
+ for j, t in enumerate(x)] for i, x in enumerate(tokens)]
+ return new_tokens
+
+def down_weight(tokens, weights, word_ids, base_emb, length, encode_func, m_token=266):
+ w, w_inv = np.unique(weights,return_inverse=True)
+
+ if np.sum(w < 1) == 0:
+ return base_emb, tokens, base_emb[0,length-1:length,:]
+ #m_token = (clip.tokenizer.end_token, 1.0) if clip.tokenizer.pad_with_end else (0,1.0)
+ #using the comma token as a masking token seems to work better than aos tokens for SD 1.x
+ m_token = (m_token, 1.0)
+
+ masked_tokens = []
+
+ masked_current = tokens
+ for i in range(len(w)):
+ if w[i] >= 1:
+ continue
+ masked_current = mask_inds(masked_current, np.where(w_inv == i)[0], m_token)
+ masked_tokens.extend(masked_current)
+
+ embs = batched_clip_encode(masked_tokens, length, encode_func, len(tokens))
+ embs = torch.cat([base_emb, embs])
+ w = w[w<=1.0]
+ w_mix = np.diff([0] + w.tolist())
+ w_mix = torch.tensor(w_mix, dtype=embs.dtype, device=embs.device).reshape((-1,1,1))
+
+ weighted_emb = (w_mix * embs).sum(axis=0, keepdim=True)
+ return weighted_emb, masked_current, weighted_emb[0,length-1:length,:]
+
+def scale_emb_to_mag(base_emb, weighted_emb):
+ norm_base = torch.linalg.norm(base_emb)
+ norm_weighted = torch.linalg.norm(weighted_emb)
+ embeddings_final = (norm_base / norm_weighted) * weighted_emb
+ return embeddings_final
+
+def recover_dist(base_emb, weighted_emb):
+ fixed_std = (base_emb.std() / weighted_emb.std()) * (weighted_emb - weighted_emb.mean())
+ embeddings_final = fixed_std + (base_emb.mean() - fixed_std.mean())
+ return embeddings_final
+
+def A1111_renorm(base_emb, weighted_emb):
+ embeddings_final = (base_emb.mean() / weighted_emb.mean()) * weighted_emb
+ return embeddings_final
+
+def advanced_encode_from_tokens(tokenized, token_normalization, weight_interpretation, encode_func, m_token=266, length=77, w_max=1.0, return_pooled=False, apply_to_pooled=False):
+ tokens = [[t for t,_,_ in x] for x in tokenized]
+ weights = [[w for _,w,_ in x] for x in tokenized]
+ word_ids = [[wid for _,_,wid in x] for x in tokenized]
+
+ #weight normalization
+ #====================
+
+ #distribute down/up weights over word lengths
+ if token_normalization.startswith("length"):
+ weights = divide_length(word_ids, weights)
+
+ #make mean of word tokens 1
+ if token_normalization.endswith("mean"):
+ weights = shift_mean_weight(word_ids, weights)
+
+ #weight interpretation
+ #=====================
+ pooled = None
+
+ if weight_interpretation == "comfy":
+ weighted_tokens = [[(t,w) for t, w in zip(x, y)] for x, y in zip(tokens, weights)]
+ weighted_emb, pooled_base = encode_func(weighted_tokens)
+ pooled = pooled_base
+ else:
+ unweighted_tokens = [[(t,1.0) for t, _,_ in x] for x in tokenized]
+ base_emb, pooled_base = encode_func(unweighted_tokens)
+
+ if weight_interpretation == "A1111":
+ weighted_emb = from_zero(weights, base_emb)
+ weighted_emb = A1111_renorm(base_emb, weighted_emb)
+ pooled = pooled_base
+
+ if weight_interpretation == "compel":
+ pos_tokens = [[(t,w) if w >= 1.0 else (t,1.0) for t, w in zip(x, y)] for x, y in zip(tokens, weights)]
+ weighted_emb, _ = encode_func(pos_tokens)
+ weighted_emb, _, pooled = down_weight(pos_tokens, weights, word_ids, weighted_emb, length, encode_func)
+
+ if weight_interpretation == "comfy++":
+ weighted_emb, tokens_down, _ = down_weight(unweighted_tokens, weights, word_ids, base_emb, length, encode_func)
+ weights = [[w if w > 1.0 else 1.0 for w in x] for x in weights]
+ #unweighted_tokens = [[(t,1.0) for t, _,_ in x] for x in tokens_down]
+ embs, pooled = from_masked(unweighted_tokens, weights, word_ids, base_emb, length, encode_func)
+ weighted_emb += embs
+
+ if weight_interpretation == "down_weight":
+ weights = scale_to_norm(weights, word_ids, w_max)
+ weighted_emb, _, pooled = down_weight(unweighted_tokens, weights, word_ids, base_emb, length, encode_func)
+
+ if return_pooled:
+ if apply_to_pooled:
+ return weighted_emb, pooled
+ else:
+ return weighted_emb, pooled_base
+ return weighted_emb, None
+
+def encode_token_weights_g(model, token_weight_pairs):
+ return model.clip_g.encode_token_weights(token_weight_pairs)
+
+def encode_token_weights_l(model, token_weight_pairs):
+ l_out, _ = model.clip_l.encode_token_weights(token_weight_pairs)
+ return l_out, None
+
+def encode_token_weights(model, token_weight_pairs, encode_func):
+ if model.layer_idx is not None:
+ model.cond_stage_model.clip_layer(model.layer_idx)
+
+ model_management.load_model_gpu(model.patcher)
+ return encode_func(model.cond_stage_model, token_weight_pairs)
+
+def prepareXL(embs_l, embs_g, pooled, clip_balance):
+ l_w = 1 - max(0, clip_balance - .5) * 2
+ g_w = 1 - max(0, .5 - clip_balance) * 2
+ if embs_l is not None:
+ return torch.cat([embs_l * l_w, embs_g * g_w], dim=-1), pooled
+ else:
+ return embs_g, pooled
+
+def advanced_encode(clip, text, token_normalization, weight_interpretation, w_max=1.0, clip_balance=.5, apply_to_pooled=True):
+ tokenized = clip.tokenize(text, return_word_ids=True)
+ if isinstance(clip.cond_stage_model, (SDXLClipModel, SDXLRefinerClipModel, SDXLClipG)):
+ embs_l = None
+ embs_g = None
+ pooled = None
+ if 'l' in tokenized and isinstance(clip.cond_stage_model, SDXLClipModel):
+ embs_l, _ = advanced_encode_from_tokens(tokenized['l'],
+ token_normalization,
+ weight_interpretation,
+ lambda x: encode_token_weights(clip, x, encode_token_weights_l),
+ w_max=w_max,
+ return_pooled=False)
+ if 'g' in tokenized:
+ embs_g, pooled = advanced_encode_from_tokens(tokenized['g'],
+ token_normalization,
+ weight_interpretation,
+ lambda x: encode_token_weights(clip, x, encode_token_weights_g),
+ w_max=w_max,
+ return_pooled=True,
+ apply_to_pooled=apply_to_pooled)
+ return prepareXL(embs_l, embs_g, pooled, clip_balance)
+ else:
+ return advanced_encode_from_tokens(tokenized['l'],
+ token_normalization,
+ weight_interpretation,
+ lambda x: (clip.encode_from_tokens({'l': x}), None),
+ w_max=w_max)
+def advanced_encode_XL(clip, text1, text2, token_normalization, weight_interpretation, w_max=1.0, clip_balance=.5, apply_to_pooled=True):
+ tokenized1 = clip.tokenize(text1, return_word_ids=True)
+ tokenized2 = clip.tokenize(text2, return_word_ids=True)
+
+ embs_l, _ = advanced_encode_from_tokens(tokenized1['l'],
+ token_normalization,
+ weight_interpretation,
+ lambda x: encode_token_weights(clip, x, encode_token_weights_l),
+ w_max=w_max,
+ return_pooled=False)
+
+ embs_g, pooled = advanced_encode_from_tokens(tokenized2['g'],
+ token_normalization,
+ weight_interpretation,
+ lambda x: encode_token_weights(clip, x, encode_token_weights_g),
+ w_max=w_max,
+ return_pooled=True,
+ apply_to_pooled=apply_to_pooled)
+
+ gcd_num = gcd(embs_l.shape[1], embs_g.shape[1])
+ repeat_l = int((embs_g.shape[1] / gcd_num) * embs_l.shape[1])
+ repeat_g = int((embs_l.shape[1] / gcd_num) * embs_g.shape[1])
+
+ return prepareXL(embs_l.expand((-1,repeat_l,-1)), embs_g.expand((-1,repeat_g,-1)), pooled, clip_balance)
+
+########################################################################################################################
+from nodes import MAX_RESOLUTION
+
+class AdvancedCLIPTextEncode:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "text": ("STRING", {"multiline": True}),
+ "clip": ("CLIP",),
+ "token_normalization": (["none", "mean", "length", "length+mean"],),
+ "weight_interpretation": (["comfy", "A1111", "compel", "comfy++", "down_weight"],),
+ # "affect_pooled": (["disable", "enable"],),
+ }}
+
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "encode"
+
+ CATEGORY = "conditioning/advanced"
+
+ def encode(self, clip, text, token_normalization, weight_interpretation, affect_pooled='disable'):
+ embeddings_final, pooled = advanced_encode(clip, text, token_normalization, weight_interpretation, w_max=1.0,
+ apply_to_pooled=affect_pooled == 'enable')
+ return ([[embeddings_final, {"pooled_output": pooled}]],)
+
+
+class AddCLIPSDXLRParams:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "conditioning": ("CONDITIONING",),
+ "width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
+ "height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
+ "ascore": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01}),
+ }}
+
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "encode"
+
+ CATEGORY = "conditioning/advanced"
+
+ def encode(self, conditioning, width, height, ascore):
+ c = []
+ for t in conditioning:
+ n = [t[0], t[1].copy()]
+ n[1]['width'] = width
+ n[1]['height'] = height
+ n[1]['aesthetic_score'] = ascore
+ c.append(n)
+ return (c,)
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/bnk_tiled_samplers.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/bnk_tiled_samplers.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3e8e0de2773003866073d9790f7b36e01b3b8b1
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/bnk_tiled_samplers.py
@@ -0,0 +1,345 @@
+# https://github.com/BlenderNeko/ComfyUI_TiledKSampler
+import sys
+import os
+import itertools
+import numpy as np
+from tqdm.auto import tqdm
+import torch
+
+#sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))
+import comfy.sd
+import comfy.controlnet
+import comfy.model_management
+import comfy.sample
+from . import bnk_tiling as tiling
+import latent_preview
+#import torch
+#import itertools
+#import numpy as np
+MAX_RESOLUTION=8192
+
+#######################
+
+def recursion_to_list(obj, attr):
+ current = obj
+ yield current
+ while True:
+ current = getattr(current, attr, None)
+ if current is not None:
+ yield current
+ else:
+ return
+
+def copy_cond(cond):
+ return [[c1,c2.copy()] for c1,c2 in cond]
+
+def slice_cond(tile_h, tile_h_len, tile_w, tile_w_len, cond, area, device):
+ tile_h_end = tile_h + tile_h_len
+ tile_w_end = tile_w + tile_w_len
+ coords = area[0] #h_len, w_len, h, w,
+ mask = area[1]
+ if coords is not None:
+ h_len, w_len, h, w = coords
+ h_end = h + h_len
+ w_end = w + w_len
+ if h < tile_h_end and h_end > tile_h and w < tile_w_end and w_end > tile_w:
+ new_h = max(0, h - tile_h)
+ new_w = max(0, w - tile_w)
+ new_h_end = min(tile_h_end, h_end - tile_h)
+ new_w_end = min(tile_w_end, w_end - tile_w)
+ cond[1]['area'] = (new_h_end - new_h, new_w_end - new_w, new_h, new_w)
+ else:
+ return (cond, True)
+ if mask is not None:
+ new_mask = tiling.get_slice(mask, tile_h,tile_h_len,tile_w,tile_w_len)
+ if new_mask.sum().to(device) == 0.0 and 'mask' in cond[1]:
+ return (cond, True)
+ else:
+ cond[1]['mask'] = new_mask
+ return (cond, False)
+
+def slice_gligen(tile_h, tile_h_len, tile_w, tile_w_len, cond, gligen):
+ tile_h_end = tile_h + tile_h_len
+ tile_w_end = tile_w + tile_w_len
+ if gligen is None:
+ return
+ gligen_type = gligen[0]
+ gligen_model = gligen[1]
+ gligen_areas = gligen[2]
+
+ gligen_areas_new = []
+ for emb, h_len, w_len, h, w in gligen_areas:
+ h_end = h + h_len
+ w_end = w + w_len
+ if h < tile_h_end and h_end > tile_h and w < tile_w_end and w_end > tile_w:
+ new_h = max(0, h - tile_h)
+ new_w = max(0, w - tile_w)
+ new_h_end = min(tile_h_end, h_end - tile_h)
+ new_w_end = min(tile_w_end, w_end - tile_w)
+ gligen_areas_new.append((emb, new_h_end - new_h, new_w_end - new_w, new_h, new_w))
+
+ if len(gligen_areas_new) == 0:
+ del cond['gligen']
+ else:
+ cond['gligen'] = (gligen_type, gligen_model, gligen_areas_new)
+
+def slice_cnet(h, h_len, w, w_len, model:comfy.controlnet.ControlBase, img):
+ if img is None:
+ img = model.cond_hint_original
+ model.cond_hint = tiling.get_slice(img, h*8, h_len*8, w*8, w_len*8).to(model.control_model.dtype).to(model.device)
+
+def slices_T2I(h, h_len, w, w_len, model:comfy.controlnet.ControlBase, img):
+ model.control_input = None
+ if img is None:
+ img = model.cond_hint_original
+ model.cond_hint = tiling.get_slice(img, h*8, h_len*8, w*8, w_len*8).float().to(model.device)
+
+# TODO: refactor some of the mess
+
+from PIL import Image
+
+def sample_common(model, add_noise, noise_seed, tile_width, tile_height, tiling_strategy, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, return_with_leftover_noise, denoise=1.0, preview=False):
+ end_at_step = min(end_at_step, steps)
+ device = comfy.model_management.get_torch_device()
+ samples = latent_image["samples"]
+ noise_mask = latent_image["noise_mask"] if "noise_mask" in latent_image else None
+ force_full_denoise = return_with_leftover_noise == "enable"
+ if add_noise == "disable":
+ noise = torch.zeros(samples.size(), dtype=samples.dtype, layout=samples.layout, device="cpu")
+ else:
+ skip = latent_image["batch_index"] if "batch_index" in latent_image else None
+ noise = comfy.sample.prepare_noise(samples, noise_seed, skip)
+
+ if noise_mask is not None:
+ noise_mask = comfy.sample.prepare_mask(noise_mask, noise.shape, device='cpu')
+
+ shape = samples.shape
+ samples = samples.clone()
+
+ tile_width = min(shape[-1] * 8, tile_width)
+ tile_height = min(shape[2] * 8, tile_height)
+
+ real_model = None
+ positive_copy = comfy.sample.convert_cond(positive)
+ negative_copy = comfy.sample.convert_cond(negative)
+ modelPatches, inference_memory = comfy.sample.get_additional_models(positive_copy, negative_copy, model.model_dtype())
+ comfy.model_management.load_models_gpu([model] + modelPatches, model.memory_required(noise.shape) + inference_memory)
+ real_model = model.model
+
+ sampler = comfy.samplers.KSampler(real_model, steps=steps, device=device, sampler=sampler_name, scheduler=scheduler, denoise=denoise, model_options=model.model_options)
+
+ if tiling_strategy != 'padded':
+ if noise_mask is not None:
+ samples += sampler.sigmas[start_at_step].cpu() * noise_mask * model.model.process_latent_out(noise)
+ else:
+ samples += sampler.sigmas[start_at_step].cpu() * model.model.process_latent_out(noise)
+
+ #cnets
+ cnets = [c['control'] for (_, c) in positive + negative if 'control' in c and isinstance(c['control'], comfy.controlnet.ControlNet)]
+ cnets = list(set([x for m in cnets for x in recursion_to_list(m, "previous_controlnet")]))
+ cnet_imgs = [
+ torch.nn.functional.interpolate(m.cond_hint_original, (shape[-2] * 8, shape[-1] * 8), mode='nearest-exact').to('cpu')
+ if m.cond_hint_original.shape[-2] != shape[-2] * 8 or m.cond_hint_original.shape[-1] != shape[-1] * 8 else None
+ for m in cnets]
+
+ #T2I
+ T2Is = [c['control'] for (_, c) in positive + negative if 'control' in c and isinstance(c['control'], comfy.controlnet.T2IAdapter)]
+ T2Is = [x for m in T2Is for x in recursion_to_list(m, "previous_controlnet")]
+ T2I_imgs = [
+ torch.nn.functional.interpolate(m.cond_hint_original, (shape[-2] * 8, shape[-1] * 8), mode='nearest-exact').to('cpu')
+ if m.cond_hint_original.shape[-2] != shape[-2] * 8 or m.cond_hint_original.shape[-1] != shape[-1] * 8 or (m.channels_in == 1 and m.cond_hint_original.shape[1] != 1) else None
+ for m in T2Is
+ ]
+ T2I_imgs = [
+ torch.mean(img, 1, keepdim=True) if img is not None and m.channels_in == 1 and m.cond_hint_original.shape[1] else img
+ for m, img in zip(T2Is, T2I_imgs)
+ ]
+
+ #cond area and mask
+ spatial_conds_pos = [
+ (c[1]['area'] if 'area' in c[1] else None,
+ comfy.sample.prepare_mask(c[1]['mask'], shape, device) if 'mask' in c[1] else None)
+ for c in positive
+ ]
+ spatial_conds_neg = [
+ (c[1]['area'] if 'area' in c[1] else None,
+ comfy.sample.prepare_mask(c[1]['mask'], shape, device) if 'mask' in c[1] else None)
+ for c in negative
+ ]
+
+ #gligen
+ gligen_pos = [
+ c[1]['gligen'] if 'gligen' in c[1] else None
+ for c in positive
+ ]
+ gligen_neg = [
+ c[1]['gligen'] if 'gligen' in c[1] else None
+ for c in negative
+ ]
+
+
+ gen = torch.manual_seed(noise_seed)
+ if tiling_strategy == 'random' or tiling_strategy == 'random strict':
+ tiles = tiling.get_tiles_and_masks_rgrid(end_at_step - start_at_step, samples.shape, tile_height, tile_width, gen)
+ elif tiling_strategy == 'padded':
+ tiles = tiling.get_tiles_and_masks_padded(end_at_step - start_at_step, samples.shape, tile_height, tile_width)
+ else:
+ tiles = tiling.get_tiles_and_masks_simple(end_at_step - start_at_step, samples.shape, tile_height, tile_width)
+
+ total_steps = sum([num_steps for img_pass in tiles for steps_list in img_pass for _,_,_,_,num_steps,_ in steps_list])
+ current_step = [0]
+
+ preview_format = "JPEG"
+ if preview_format not in ["JPEG", "PNG"]:
+ preview_format = "JPEG"
+ previewer = None
+ if preview:
+ previewer = latent_preview.get_previewer(device, model.model.latent_format)
+
+
+ with tqdm(total=total_steps) as pbar_tqdm:
+ pbar = comfy.utils.ProgressBar(total_steps)
+
+ def callback(step, x0, x, total_steps):
+ current_step[0] += 1
+ preview_bytes = None
+ if previewer:
+ preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0)
+ pbar.update_absolute(current_step[0], preview=preview_bytes)
+ pbar_tqdm.update(1)
+
+ if tiling_strategy == "random strict":
+ samples_next = samples.clone()
+ for img_pass in tiles:
+ for i in range(len(img_pass)):
+ for tile_h, tile_h_len, tile_w, tile_w_len, tile_steps, tile_mask in img_pass[i]:
+ tiled_mask = None
+ if noise_mask is not None:
+ tiled_mask = tiling.get_slice(noise_mask, tile_h, tile_h_len, tile_w, tile_w_len).to(device)
+ if tile_mask is not None:
+ if tiled_mask is not None:
+ tiled_mask *= tile_mask.to(device)
+ else:
+ tiled_mask = tile_mask.to(device)
+
+ if tiling_strategy == 'padded' or tiling_strategy == 'random strict':
+ tile_h, tile_h_len, tile_w, tile_w_len, tiled_mask = tiling.mask_at_boundary( tile_h, tile_h_len, tile_w, tile_w_len,
+ tile_height, tile_width, samples.shape[-2], samples.shape[-1],
+ tiled_mask, device)
+
+
+ if tiled_mask is not None and tiled_mask.sum().cpu() == 0.0:
+ continue
+
+ tiled_latent = tiling.get_slice(samples, tile_h, tile_h_len, tile_w, tile_w_len).to(device)
+
+ if tiling_strategy == 'padded':
+ tiled_noise = tiling.get_slice(noise, tile_h, tile_h_len, tile_w, tile_w_len).to(device)
+ else:
+ if tiled_mask is None or noise_mask is None:
+ tiled_noise = torch.zeros_like(tiled_latent)
+ else:
+ tiled_noise = tiling.get_slice(noise, tile_h, tile_h_len, tile_w, tile_w_len).to(device) * (1 - tiled_mask)
+
+ #TODO: all other condition based stuff like area sets and GLIGEN should also happen here
+
+ #cnets
+ for m, img in zip(cnets, cnet_imgs):
+ slice_cnet(tile_h, tile_h_len, tile_w, tile_w_len, m, img)
+
+ #T2I
+ for m, img in zip(T2Is, T2I_imgs):
+ slices_T2I(tile_h, tile_h_len, tile_w, tile_w_len, m, img)
+
+ pos = [c.copy() for c in positive_copy]#copy_cond(positive_copy)
+ neg = [c.copy() for c in negative_copy]#copy_cond(negative_copy)
+
+ #cond areas
+ pos = [slice_cond(tile_h, tile_h_len, tile_w, tile_w_len, c, area, device) for c, area in zip(pos, spatial_conds_pos)]
+ pos = [c for c, ignore in pos if not ignore]
+ neg = [slice_cond(tile_h, tile_h_len, tile_w, tile_w_len, c, area, device) for c, area in zip(neg, spatial_conds_neg)]
+ neg = [c for c, ignore in neg if not ignore]
+
+ #gligen
+ for cond, gligen in zip(pos, gligen_pos):
+ slice_gligen(tile_h, tile_h_len, tile_w, tile_w_len, cond, gligen)
+ for cond, gligen in zip(neg, gligen_neg):
+ slice_gligen(tile_h, tile_h_len, tile_w, tile_w_len, cond, gligen)
+
+ tile_result = sampler.sample(tiled_noise, pos, neg, cfg=cfg, latent_image=tiled_latent, start_step=start_at_step + i * tile_steps, last_step=start_at_step + i*tile_steps + tile_steps, force_full_denoise=force_full_denoise and i+1 == end_at_step - start_at_step, denoise_mask=tiled_mask, callback=callback, disable_pbar=True, seed=noise_seed)
+ tile_result = tile_result.cpu()
+ if tiled_mask is not None:
+ tiled_mask = tiled_mask.cpu()
+ if tiling_strategy == "random strict":
+ tiling.set_slice(samples_next, tile_result, tile_h, tile_h_len, tile_w, tile_w_len, tiled_mask)
+ else:
+ tiling.set_slice(samples, tile_result, tile_h, tile_h_len, tile_w, tile_w_len, tiled_mask)
+ if tiling_strategy == "random strict":
+ samples = samples_next.clone()
+
+
+ comfy.sample.cleanup_additional_models(modelPatches)
+
+ out = latent_image.copy()
+ out["samples"] = samples.cpu()
+ return (out, )
+
+
+class TiledKSampler:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required":
+ {"model": ("MODEL",),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "tile_width": ("INT", {"default": 512, "min": 256, "max": MAX_RESOLUTION, "step": 64}),
+ "tile_height": ("INT", {"default": 512, "min": 256, "max": MAX_RESOLUTION, "step": 64}),
+ "tiling_strategy": (["random", "random strict", "padded", 'simple'], ),
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
+ "positive": ("CONDITIONING", ),
+ "negative": ("CONDITIONING", ),
+ "latent_image": ("LATENT", ),
+ "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
+ }}
+
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "sample"
+
+ CATEGORY = "sampling"
+
+ def sample(self, model, seed, tile_width, tile_height, tiling_strategy, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise):
+ steps_total = int(steps / denoise)
+ return sample_common(model, 'enable', seed, tile_width, tile_height, tiling_strategy, steps_total, cfg, sampler_name, scheduler, positive, negative, latent_image, steps_total-steps, steps_total, 'disable', denoise=1.0, preview=True)
+
+class TiledKSamplerAdvanced:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required":
+ {"model": ("MODEL",),
+ "add_noise": (["enable", "disable"], ),
+ "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "tile_width": ("INT", {"default": 512, "min": 256, "max": MAX_RESOLUTION, "step": 64}),
+ "tile_height": ("INT", {"default": 512, "min": 256, "max": MAX_RESOLUTION, "step": 64}),
+ "tiling_strategy": (["random", "random strict", "padded", 'simple'], ),
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
+ "positive": ("CONDITIONING", ),
+ "negative": ("CONDITIONING", ),
+ "latent_image": ("LATENT", ),
+ "start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
+ "end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
+ "return_with_leftover_noise": (["disable", "enable"], ),
+ "preview": (["disable", "enable"], ),
+ }}
+
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "sample"
+
+ CATEGORY = "sampling"
+
+ def sample(self, model, add_noise, noise_seed, tile_width, tile_height, tiling_strategy, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, return_with_leftover_noise, preview, denoise=1.0):
+ return sample_common(model, add_noise, noise_seed, tile_width, tile_height, tiling_strategy, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, return_with_leftover_noise, denoise=1.0, preview= preview == 'enable')
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/bnk_tiling.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/bnk_tiling.py
new file mode 100644
index 0000000000000000000000000000000000000000..096f8baa1a1a639e3ed251e415a95aa14bcf8753
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/bnk_tiling.py
@@ -0,0 +1,175 @@
+import torch
+import itertools
+import numpy as np
+
+def grouper(n, iterable):
+ it = iter(iterable)
+ while True:
+ chunk = list(itertools.islice(it, n))
+ if not chunk:
+ return
+ yield chunk
+
+def create_batches(n, iterable):
+ groups = itertools.groupby(iterable, key= lambda x: (x[1], x[3]))
+ for _, x in groups:
+ for y in grouper(n, x):
+ yield y
+
+
+def get_slice(tensor, h, h_len, w, w_len):
+ t = tensor.narrow(-2, h, h_len)
+ t = t.narrow(-1, w, w_len)
+ return t
+
+def set_slice(tensor1,tensor2, h, h_len, w, w_len, mask=None):
+ if mask is not None:
+ tensor1[:,:,h:h+h_len,w:w+w_len] = tensor1[:,:,h:h+h_len,w:w+w_len] * (1 - mask) + tensor2 * mask
+ else:
+ tensor1[:,:,h:h+h_len,w:w+w_len] = tensor2
+
+def get_tiles_and_masks_simple(steps, latent_shape, tile_height, tile_width):
+ latent_size_h = latent_shape[-2]
+ latent_size_w = latent_shape[-1]
+ tile_size_h = int(tile_height // 8)
+ tile_size_w = int(tile_width // 8)
+
+ h = np.arange(0,latent_size_h, tile_size_h)
+ w = np.arange(0,latent_size_w, tile_size_w)
+
+ def create_tile(hs, ws, i, j):
+ h = int(hs[i])
+ w = int(ws[j])
+ h_len = min(tile_size_h, latent_size_h - h)
+ w_len = min(tile_size_w, latent_size_w - w)
+ return (h, h_len, w, w_len, steps, None)
+
+ passes = [
+ [[create_tile(h, w, i, j) for i in range(len(h)) for j in range(len(w))]],
+ ]
+ return passes
+
+def get_tiles_and_masks_padded(steps, latent_shape, tile_height, tile_width):
+ batch_size = latent_shape[0]
+ latent_size_h = latent_shape[-2]
+ latent_size_w = latent_shape[-1]
+
+ tile_size_h = int(tile_height // 8)
+ tile_size_h = int((tile_size_h // 4) * 4)
+ tile_size_w = int(tile_width // 8)
+ tile_size_w = int((tile_size_w // 4) * 4)
+
+ #masks
+ mask_h = [0,tile_size_h // 4, tile_size_h - tile_size_h // 4, tile_size_h]
+ mask_w = [0,tile_size_w // 4, tile_size_w - tile_size_w // 4, tile_size_w]
+ masks = [[] for _ in range(3)]
+ for i in range(3):
+ for j in range(3):
+ mask = torch.zeros((batch_size,1,tile_size_h, tile_size_w), dtype=torch.float32, device='cpu')
+ mask[:,:,mask_h[i]:mask_h[i+1],mask_w[j]:mask_w[j+1]] = 1.0
+ masks[i].append(mask)
+
+ def create_mask(h_ind, w_ind, h_ind_max, w_ind_max, mask_h, mask_w, h_len, w_len):
+ mask = masks[1][1]
+ if not (h_ind == 0 or h_ind == h_ind_max or w_ind == 0 or w_ind == w_ind_max):
+ return get_slice(mask, 0, h_len, 0, w_len)
+ mask = mask.clone()
+ if h_ind == 0 and mask_h:
+ mask += masks[0][1]
+ if h_ind == h_ind_max and mask_h:
+ mask += masks[2][1]
+ if w_ind == 0 and mask_w:
+ mask += masks[1][0]
+ if w_ind == w_ind_max and mask_w:
+ mask += masks[1][2]
+ if h_ind == 0 and w_ind == 0 and mask_h and mask_w:
+ mask += masks[0][0]
+ if h_ind == 0 and w_ind == w_ind_max and mask_h and mask_w:
+ mask += masks[0][2]
+ if h_ind == h_ind_max and w_ind == 0 and mask_h and mask_w:
+ mask += masks[2][0]
+ if h_ind == h_ind_max and w_ind == w_ind_max and mask_h and mask_w:
+ mask += masks[2][2]
+ return get_slice(mask, 0, h_len, 0, w_len)
+
+ h = np.arange(0,latent_size_h, tile_size_h)
+ h_shift = np.arange(tile_size_h // 2, latent_size_h - tile_size_h // 2, tile_size_h)
+ w = np.arange(0,latent_size_w, tile_size_w)
+ w_shift = np.arange(tile_size_w // 2, latent_size_w - tile_size_h // 2, tile_size_w)
+
+
+ def create_tile(hs, ws, mask_h, mask_w, i, j):
+ h = int(hs[i])
+ w = int(ws[j])
+ h_len = min(tile_size_h, latent_size_h - h)
+ w_len = min(tile_size_w, latent_size_w - w)
+ mask = create_mask(i,j,len(hs)-1, len(ws)-1, mask_h, mask_w, h_len, w_len)
+ return (h, h_len, w, w_len, steps, mask)
+
+ passes = [
+ [[create_tile(h, w, True, True, i, j) for i in range(len(h)) for j in range(len(w))]],
+ [[create_tile(h_shift, w, False, True, i, j) for i in range(len(h_shift)) for j in range(len(w))]],
+ [[create_tile(h, w_shift, True, False, i, j) for i in range(len(h)) for j in range(len(w_shift))]],
+ [[create_tile(h_shift, w_shift, False, False, i,j) for i in range(len(h_shift)) for j in range(len(w_shift))]],
+ ]
+
+ return passes
+
+def mask_at_boundary(h, h_len, w, w_len, tile_size_h, tile_size_w, latent_size_h, latent_size_w, mask, device='cpu'):
+ tile_size_h = int(tile_size_h // 8)
+ tile_size_w = int(tile_size_w // 8)
+
+ if (h_len == tile_size_h or h_len == latent_size_h) and (w_len == tile_size_w or w_len == latent_size_w):
+ return h, h_len, w, w_len, mask
+ h_offset = min(0, latent_size_h - (h + tile_size_h))
+ w_offset = min(0, latent_size_w - (w + tile_size_w))
+ new_mask = torch.zeros((1,1,tile_size_h, tile_size_w), dtype=torch.float32, device=device)
+ new_mask[:,:,-h_offset:h_len if h_offset == 0 else tile_size_h, -w_offset:w_len if w_offset == 0 else tile_size_w] = 1.0 if mask is None else mask
+ return h + h_offset, tile_size_h, w + w_offset, tile_size_w, new_mask
+
+def get_tiles_and_masks_rgrid(steps, latent_shape, tile_height, tile_width, generator):
+
+ def calc_coords(latent_size, tile_size, jitter):
+ tile_coords = int((latent_size + jitter - 1) // tile_size + 1)
+ tile_coords = [np.clip(tile_size * c - jitter, 0, latent_size) for c in range(tile_coords + 1)]
+ tile_coords = [(c1, c2-c1) for c1, c2 in zip(tile_coords, tile_coords[1:])]
+ return tile_coords
+
+ #calc stuff
+ batch_size = latent_shape[0]
+ latent_size_h = latent_shape[-2]
+ latent_size_w = latent_shape[-1]
+ tile_size_h = int(tile_height // 8)
+ tile_size_w = int(tile_width // 8)
+
+ tiles_all = []
+
+ for s in range(steps):
+ rands = torch.rand((2,), dtype=torch.float32, generator=generator, device='cpu').numpy()
+
+ jitter_w1 = int(rands[0] * tile_size_w)
+ jitter_w2 = int(((rands[0] + .5) % 1.0) * tile_size_w)
+ jitter_h1 = int(rands[1] * tile_size_h)
+ jitter_h2 = int(((rands[1] + .5) % 1.0) * tile_size_h)
+
+ #calc number of tiles
+ tiles_h = [
+ calc_coords(latent_size_h, tile_size_h, jitter_h1),
+ calc_coords(latent_size_h, tile_size_h, jitter_h2)
+ ]
+ tiles_w = [
+ calc_coords(latent_size_w, tile_size_w, jitter_w1),
+ calc_coords(latent_size_w, tile_size_w, jitter_w2)
+ ]
+
+ tiles = []
+ if s % 2 == 0:
+ for i, h in enumerate(tiles_h[0]):
+ for w in tiles_w[i%2]:
+ tiles.append((int(h[0]), int(h[1]), int(w[0]), int(w[1]), 1, None))
+ else:
+ for i, w in enumerate(tiles_w[0]):
+ for h in tiles_h[i%2]:
+ tiles.append((int(h[0]), int(h[1]), int(w[0]), int(w[1]), 1, None))
+ tiles_all.append(tiles)
+ return [tiles_all]
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/cg_mixed_seed_noise.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/cg_mixed_seed_noise.py
new file mode 100644
index 0000000000000000000000000000000000000000..b670838039908319db131827b48942d63ed5aaa0
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/cg_mixed_seed_noise.py
@@ -0,0 +1,16 @@
+# https://github.com/chrisgoringe/cg-noise
+import torch
+
+def get_mixed_noise_function(original_noise_function, variation_seed, variation_weight):
+ def prepare_mixed_noise(latent_image:torch.Tensor, seed, batch_inds):
+ single_image_latent = latent_image[0].unsqueeze_(0)
+ different_noise = original_noise_function(single_image_latent, variation_seed, batch_inds)
+ original_noise = original_noise_function(single_image_latent, seed, batch_inds)
+ if latent_image.shape[0]==1:
+ mixed_noise = original_noise * (1.0-variation_weight) + different_noise * (variation_weight)
+ else:
+ mixed_noise = torch.empty_like(latent_image)
+ for i in range(latent_image.shape[0]):
+ mixed_noise[i] = original_noise * (1.0-variation_weight*i) + different_noise * (variation_weight*i)
+ return mixed_noise
+ return prepare_mixed_noise
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/city96_latent_upscaler.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/city96_latent_upscaler.py
new file mode 100644
index 0000000000000000000000000000000000000000..219fd68ae2acd9ee2446971a011f0e018af7747d
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/city96_latent_upscaler.py
@@ -0,0 +1,94 @@
+# https://github.com/city96/SD-Latent-Upscaler
+# check if any issues on weights and updates and local install for same
+import torch
+import torch.nn as nn
+from safetensors.torch import load_file
+from huggingface_hub import hf_hub_download
+
+class Upscaler(nn.Module):
+ """
+ Basic NN layout, ported from:
+ https://github.com/city96/SD-Latent-Upscaler/blob/main/upscaler.py
+ """
+ version = 2.1 # network revision
+ def head(self):
+ return [
+ nn.Conv2d(self.chan, self.size, kernel_size=self.krn, padding=self.pad),
+ nn.ReLU(),
+ nn.Upsample(scale_factor=self.fac, mode="nearest"),
+ nn.ReLU(),
+ ]
+ def core(self):
+ layers = []
+ for _ in range(self.depth):
+ layers += [
+ nn.Conv2d(self.size, self.size, kernel_size=self.krn, padding=self.pad),
+ nn.ReLU(),
+ ]
+ return layers
+ def tail(self):
+ return [
+ nn.Conv2d(self.size, self.chan, kernel_size=self.krn, padding=self.pad),
+ ]
+
+ def __init__(self, fac, depth=16):
+ super().__init__()
+ self.size = 64 # Conv2d size
+ self.chan = 4 # in/out channels
+ self.depth = depth # no. of layers
+ self.fac = fac # scale factor
+ self.krn = 3 # kernel size
+ self.pad = 1 # padding
+
+ self.sequential = nn.Sequential(
+ *self.head(),
+ *self.core(),
+ *self.tail(),
+ )
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return self.sequential(x)
+
+
+class LatentUpscaler:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "samples": ("LATENT", ),
+ "latent_ver": (["v1", "xl"],),
+ "scale_factor": (["1.25", "1.5", "2.0"],),
+ }
+ }
+
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "upscale"
+ CATEGORY = "latent"
+
+ def upscale(self, samples, latent_ver, scale_factor):
+ model = Upscaler(scale_factor)
+ weights = str(hf_hub_download(
+ repo_id="city96/SD-Latent-Upscaler",
+ filename=f"latent-upscaler-v{model.version}_SD{latent_ver}-x{scale_factor}.safetensors")
+ )
+ # weights = f"./latent-upscaler-v{model.version}_SD{latent_ver}-x{scale_factor}.safetensors"
+
+ model.load_state_dict(load_file(weights))
+ lt = samples["samples"]
+ lt = model(lt)
+ del model
+ if "noise_mask" in samples.keys():
+ # expand the noise mask to the same shape as the latent
+ mask = torch.nn.functional.interpolate(samples['noise_mask'], scale_factor=float(scale_factor), mode='bicubic')
+ return ({"samples": lt, "noise_mask": mask},)
+ return ({"samples": lt},)
+NODE_CLASS_MAPPINGS = {
+ "LatentUpscaler": LatentUpscaler,
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LatentUpscaler": "EFF_C Latent Upscaler"
+}
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/sd15_resizer.pt b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/sd15_resizer.pt
new file mode 100644
index 0000000000000000000000000000000000000000..bca5b91b2b15a8b1e5ec2e151252c483da441b80
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/sd15_resizer.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8fa1cb8168b305d556f2e8178c820b0a6956f4ba84ebc9443fac69be43ffd6fe
+size 12628977
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/sdxl_resizer.pt b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/sdxl_resizer.pt
new file mode 100644
index 0000000000000000000000000000000000000000..b72dac3623a86fb63107fa0fda6d9c05ee6e0dff
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/sdxl_resizer.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0bca261c96e136cb9e2f330f40386e6cbcaaf464cd39e9f7752c9e7b32e825e8
+size 12628977
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/smZ_cfg_denoiser.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/smZ_cfg_denoiser.py
new file mode 100644
index 0000000000000000000000000000000000000000..967712cc02d09f416df77d0892ddfdcc23b5e275
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/smZ_cfg_denoiser.py
@@ -0,0 +1,321 @@
+# https://github.com/shiimizu/ComfyUI_smZNodes
+import comfy
+import torch
+from typing import List
+import comfy.sample
+from comfy import model_base, model_management
+from comfy.samplers import KSampler, KSamplerX0Inpaint, wrap_model
+#from comfy.k_diffusion.external import CompVisDenoiser
+import nodes
+import inspect
+import functools
+import importlib
+import os
+import re
+import itertools
+
+import torch
+from comfy import model_management
+
+def catenate_conds(conds):
+ if not isinstance(conds[0], dict):
+ return torch.cat(conds)
+
+ return {key: torch.cat([x[key] for x in conds]) for key in conds[0].keys()}
+
+
+def subscript_cond(cond, a, b):
+ if not isinstance(cond, dict):
+ return cond[a:b]
+
+ return {key: vec[a:b] for key, vec in cond.items()}
+
+
+def pad_cond(tensor, repeats, empty):
+ if not isinstance(tensor, dict):
+ return torch.cat([tensor, empty.repeat((tensor.shape[0], repeats, 1)).to(device=tensor.device)], axis=1)
+
+ tensor['crossattn'] = pad_cond(tensor['crossattn'], repeats, empty)
+ return tensor
+
+
+class CFGDenoiser(torch.nn.Module):
+ """
+ Classifier free guidance denoiser. A wrapper for stable diffusion model (specifically for unet)
+ that can take a noisy picture and produce a noise-free picture using two guidances (prompts)
+ instead of one. Originally, the second prompt is just an empty string, but we use non-empty
+ negative prompt.
+ """
+
+ def __init__(self, model):
+ super().__init__()
+ self.inner_model = model
+ self.model_wrap = None
+ self.mask = None
+ self.nmask = None
+ self.init_latent = None
+ self.steps = None
+ """number of steps as specified by user in UI"""
+
+ self.total_steps = None
+ """expected number of calls to denoiser calculated from self.steps and specifics of the selected sampler"""
+
+ self.step = 0
+ self.image_cfg_scale = None
+ self.padded_cond_uncond = False
+ self.sampler = None
+ self.model_wrap = None
+ self.p = None
+ self.mask_before_denoising = False
+
+
+ def combine_denoised(self, x_out, conds_list, uncond, cond_scale):
+ denoised_uncond = x_out[-uncond.shape[0]:]
+ denoised = torch.clone(denoised_uncond)
+
+ for i, conds in enumerate(conds_list):
+ for cond_index, weight in conds:
+ denoised[i] += (x_out[cond_index] - denoised_uncond[i]) * (weight * cond_scale)
+
+ return denoised
+
+ def combine_denoised_for_edit_model(self, x_out, cond_scale):
+ out_cond, out_img_cond, out_uncond = x_out.chunk(3)
+ denoised = out_uncond + cond_scale * (out_cond - out_img_cond) + self.image_cfg_scale * (out_img_cond - out_uncond)
+
+ return denoised
+
+ def get_pred_x0(self, x_in, x_out, sigma):
+ return x_out
+
+ def update_inner_model(self):
+ self.model_wrap = None
+
+ c, uc = self.p.get_conds()
+ self.sampler.sampler_extra_args['cond'] = c
+ self.sampler.sampler_extra_args['uncond'] = uc
+
+ def forward(self, x, sigma, uncond, cond, cond_scale, s_min_uncond, image_cond):
+ model_management.throw_exception_if_processing_interrupted()
+
+ is_edit_model = False
+
+ conds_list, tensor = cond
+ assert not is_edit_model or all(len(conds) == 1 for conds in conds_list), "AND is not supported for InstructPix2Pix checkpoint (unless using Image CFG scale = 1.0)"
+
+ if self.mask_before_denoising and self.mask is not None:
+ x = self.init_latent * self.mask + self.nmask * x
+
+ batch_size = len(conds_list)
+ repeats = [len(conds_list[i]) for i in range(batch_size)]
+
+ if False:
+ image_uncond = torch.zeros_like(image_cond)
+ make_condition_dict = lambda c_crossattn, c_adm: {"c_crossattn": c_crossattn, "c_adm": c_adm, 'transformer_options': {'from_smZ': True}} # pylint: disable=C3001
+ else:
+ image_uncond = image_cond
+ if isinstance(uncond, dict):
+ make_condition_dict = lambda c_crossattn, c_concat: {**c_crossattn, "c_concat": None, "c_adm": x.c_adm, 'transformer_options': {'from_smZ': True}}
+ else:
+ make_condition_dict = lambda c_crossattn, c_concat: {"c_crossattn": c_crossattn, "c_concat": None, "c_adm": x.c_adm, 'transformer_options': {'from_smZ': True}}
+
+ if not is_edit_model:
+ x_in = torch.cat([torch.stack([x[i] for _ in range(n)]) for i, n in enumerate(repeats)] + [x])
+ sigma_in = torch.cat([torch.stack([sigma[i] for _ in range(n)]) for i, n in enumerate(repeats)] + [sigma])
+ image_cond_in = torch.cat([torch.stack([image_cond[i] for _ in range(n)]) for i, n in enumerate(repeats)] + [image_uncond])
+ else:
+ x_in = torch.cat([torch.stack([x[i] for _ in range(n)]) for i, n in enumerate(repeats)] + [x] + [x])
+ sigma_in = torch.cat([torch.stack([sigma[i] for _ in range(n)]) for i, n in enumerate(repeats)] + [sigma] + [sigma])
+ image_cond_in = torch.cat([torch.stack([image_cond[i] for _ in range(n)]) for i, n in enumerate(repeats)] + [image_uncond] + [torch.zeros_like(self.init_latent)])
+
+ skip_uncond = False
+
+ # alternating uncond allows for higher thresholds without the quality loss normally expected from raising it
+ if self.step % 2 and s_min_uncond > 0 and sigma[0] < s_min_uncond and not is_edit_model:
+ skip_uncond = True
+ x_in = x_in[:-batch_size]
+ sigma_in = sigma_in[:-batch_size]
+
+ if tensor.shape[1] == uncond.shape[1] or skip_uncond:
+ if is_edit_model:
+ cond_in = catenate_conds([tensor, uncond, uncond])
+ elif skip_uncond:
+ cond_in = tensor
+ else:
+ cond_in = catenate_conds([tensor, uncond])
+
+ x_out = torch.zeros_like(x_in)
+ for batch_offset in range(0, x_out.shape[0], batch_size):
+ a = batch_offset
+ b = a + batch_size
+ x_out[a:b] = self.inner_model(x_in[a:b], sigma_in[a:b], **make_condition_dict(subscript_cond(cond_in, a, b), image_cond_in[a:b]))
+ else:
+ x_out = torch.zeros_like(x_in)
+ for batch_offset in range(0, tensor.shape[0], batch_size):
+ a = batch_offset
+ b = min(a + batch_size, tensor.shape[0])
+
+ if not is_edit_model:
+ c_crossattn = subscript_cond(tensor, a, b)
+ else:
+ c_crossattn = torch.cat([tensor[a:b]], uncond)
+
+ x_out[a:b] = self.inner_model(x_in[a:b], sigma_in[a:b], **make_condition_dict(c_crossattn, image_cond_in[a:b]))
+
+ if not skip_uncond:
+ x_out[-uncond.shape[0]:] = self.inner_model(x_in[-uncond.shape[0]:], sigma_in[-uncond.shape[0]:], **make_condition_dict(uncond, image_cond_in[-uncond.shape[0]:]))
+
+ denoised_image_indexes = [x[0][0] for x in conds_list]
+ if skip_uncond:
+ fake_uncond = torch.cat([x_out[i:i+1] for i in denoised_image_indexes])
+ x_out = torch.cat([x_out, fake_uncond]) # we skipped uncond denoising, so we put cond-denoised image to where the uncond-denoised image should be
+
+ if is_edit_model:
+ denoised = self.combine_denoised_for_edit_model(x_out, cond_scale)
+ elif skip_uncond:
+ denoised = self.combine_denoised(x_out, conds_list, uncond, 1.0)
+ else:
+ denoised = self.combine_denoised(x_out, conds_list, uncond, cond_scale)
+
+ if not self.mask_before_denoising and self.mask is not None:
+ denoised = self.init_latent * self.mask + self.nmask * denoised
+
+ self.step += 1
+ del x_out
+ return denoised
+
+# ========================================================================
+
+def expand(tensor1, tensor2):
+ def adjust_tensor_shape(tensor_small, tensor_big):
+ # Calculate replication factor
+ # -(-a // b) is ceiling of division without importing math.ceil
+ replication_factor = -(-tensor_big.size(1) // tensor_small.size(1))
+
+ # Use repeat to extend tensor_small
+ tensor_small_extended = tensor_small.repeat(1, replication_factor, 1)
+
+ # Take the rows of the extended tensor_small to match tensor_big
+ tensor_small_matched = tensor_small_extended[:, :tensor_big.size(1), :]
+
+ return tensor_small_matched
+
+ # Check if their second dimensions are different
+ if tensor1.size(1) != tensor2.size(1):
+ # Check which tensor has the smaller second dimension and adjust its shape
+ if tensor1.size(1) < tensor2.size(1):
+ tensor1 = adjust_tensor_shape(tensor1, tensor2)
+ else:
+ tensor2 = adjust_tensor_shape(tensor2, tensor1)
+ return (tensor1, tensor2)
+
+def _find_outer_instance(target, target_type):
+ import inspect
+ frame = inspect.currentframe()
+ while frame:
+ if target in frame.f_locals:
+ found = frame.f_locals[target]
+ if isinstance(found, target_type) and found != 1: # steps == 1
+ return found
+ frame = frame.f_back
+ return None
+
+# ========================================================================
+def bounded_modulo(number, modulo_value):
+ return number if number < modulo_value else modulo_value
+
+def calc_cond(c, current_step):
+ """Group by smZ conds that may do prompt-editing / regular conds / comfy conds."""
+ _cond = []
+ # Group by conds from smZ
+ fn=lambda x : x[1].get("from_smZ", None) is not None
+ an_iterator = itertools.groupby(c, fn )
+ for key, group in an_iterator:
+ ls=list(group)
+ # Group by prompt-editing conds
+ fn2=lambda x : x[1].get("smZid", None)
+ an_iterator2 = itertools.groupby(ls, fn2)
+ for key2, group2 in an_iterator2:
+ ls2=list(group2)
+ if key2 is not None:
+ orig_len = ls2[0][1].get('orig_len', 1)
+ i = bounded_modulo(current_step, orig_len - 1)
+ _cond = _cond + [ls2[i]]
+ else:
+ _cond = _cond + ls2
+ return _cond
+
+class CFGNoisePredictor(torch.nn.Module):
+ def __init__(self, model):
+ super().__init__()
+ self.ksampler = _find_outer_instance('self', comfy.samplers.KSampler)
+ self.step = 0
+ self.orig = comfy.samplers.CFGNoisePredictor(model) #CFGNoisePredictorOrig(model)
+ self.inner_model = model
+ self.inner_model2 = CFGDenoiser(model.apply_model)
+ self.inner_model2.num_timesteps = model.num_timesteps
+ self.inner_model2.device = self.ksampler.device if hasattr(self.ksampler, "device") else None
+ self.s_min_uncond = 0.0
+ self.alphas_cumprod = model.alphas_cumprod
+ self.c_adm = None
+ self.init_cond = None
+ self.init_uncond = None
+ self.is_prompt_editing_u = False
+ self.is_prompt_editing_c = False
+
+ def apply_model(self, x, timestep, cond, uncond, cond_scale, cond_concat=None, model_options={}, seed=None):
+
+ cc=calc_cond(cond, self.step)
+ uu=calc_cond(uncond, self.step)
+ self.step += 1
+
+ if (any([p[1].get('from_smZ', False) for p in cc]) or
+ any([p[1].get('from_smZ', False) for p in uu])):
+ if model_options.get('transformer_options',None) is None:
+ model_options['transformer_options'] = {}
+ model_options['transformer_options']['from_smZ'] = True
+
+ # Only supports one cond
+ for ix in range(len(cc)):
+ if cc[ix][1].get('from_smZ', False):
+ cc = [cc[ix]]
+ break
+ for ix in range(len(uu)):
+ if uu[ix][1].get('from_smZ', False):
+ uu = [uu[ix]]
+ break
+ c=cc[0][1]
+ u=uu[0][1]
+ _cc = cc[0][0]
+ _uu = uu[0][0]
+ if c.get("adm_encoded", None) is not None:
+ self.c_adm = torch.cat([c['adm_encoded'], u['adm_encoded']])
+ # SDXL. Need to pad with repeats
+ _cc, _uu = expand(_cc, _uu)
+ _uu, _cc = expand(_uu, _cc)
+ x.c_adm = self.c_adm
+ conds_list = c.get('conds_list', [[(0, 1.0)]])
+ image_cond = txt2img_image_conditioning(None, x)
+ out = self.inner_model2(x, timestep, cond=(conds_list, _cc), uncond=_uu, cond_scale=cond_scale, s_min_uncond=self.s_min_uncond, image_cond=image_cond)
+ return out
+
+def txt2img_image_conditioning(sd_model, x, width=None, height=None):
+ return x.new_zeros(x.shape[0], 5, 1, 1, dtype=x.dtype, device=x.device)
+
+# =======================================================================================
+
+def set_model_k(self: KSampler):
+ self.model_denoise = CFGNoisePredictor(self.model) # main change
+ if ((getattr(self.model, "parameterization", "") == "v") or
+ (getattr(self.model, "model_type", -1) == model_base.ModelType.V_PREDICTION)):
+ self.model_wrap = wrap_model(self.model_denoise, quantize=True)
+ self.model_wrap.parameterization = getattr(self.model, "parameterization", "v")
+ else:
+ self.model_wrap = wrap_model(self.model_denoise, quantize=True)
+ self.model_wrap.parameterization = getattr(self.model, "parameterization", "eps")
+ self.model_k = KSamplerX0Inpaint(self.model_wrap)
+
+class SDKSampler(comfy.samplers.KSampler):
+ def __init__(self, *args, **kwargs):
+ super(SDKSampler, self).__init__(*args, **kwargs)
+ set_model_k(self)
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/smZ_rng_source.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/smZ_rng_source.py
new file mode 100644
index 0000000000000000000000000000000000000000..54e33ca6bc9dc1cf3b4030fee18b4fa505d3a53b
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/smZ_rng_source.py
@@ -0,0 +1,140 @@
+# https://github.com/shiimizu/ComfyUI_smZNodes
+import numpy as np
+
+philox_m = [0xD2511F53, 0xCD9E8D57]
+philox_w = [0x9E3779B9, 0xBB67AE85]
+
+two_pow32_inv = np.array([2.3283064e-10], dtype=np.float32)
+two_pow32_inv_2pi = np.array([2.3283064e-10 * 6.2831855], dtype=np.float32)
+
+
+def uint32(x):
+ """Converts (N,) np.uint64 array into (2, N) np.unit32 array."""
+ return x.view(np.uint32).reshape(-1, 2).transpose(1, 0)
+
+
+def philox4_round(counter, key):
+ """A single round of the Philox 4x32 random number generator."""
+
+ v1 = uint32(counter[0].astype(np.uint64) * philox_m[0])
+ v2 = uint32(counter[2].astype(np.uint64) * philox_m[1])
+
+ counter[0] = v2[1] ^ counter[1] ^ key[0]
+ counter[1] = v2[0]
+ counter[2] = v1[1] ^ counter[3] ^ key[1]
+ counter[3] = v1[0]
+
+
+def philox4_32(counter, key, rounds=10):
+ """Generates 32-bit random numbers using the Philox 4x32 random number generator.
+
+ Parameters:
+ counter (numpy.ndarray): A 4xN array of 32-bit integers representing the counter values (offset into generation).
+ key (numpy.ndarray): A 2xN array of 32-bit integers representing the key values (seed).
+ rounds (int): The number of rounds to perform.
+
+ Returns:
+ numpy.ndarray: A 4xN array of 32-bit integers containing the generated random numbers.
+ """
+
+ for _ in range(rounds - 1):
+ philox4_round(counter, key)
+
+ key[0] = key[0] + philox_w[0]
+ key[1] = key[1] + philox_w[1]
+
+ philox4_round(counter, key)
+ return counter
+
+
+def box_muller(x, y):
+ """Returns just the first out of two numbers generated by Box–Muller transform algorithm."""
+ u = x * two_pow32_inv + two_pow32_inv / 2
+ v = y * two_pow32_inv_2pi + two_pow32_inv_2pi / 2
+
+ s = np.sqrt(-2.0 * np.log(u))
+
+ r1 = s * np.sin(v)
+ return r1.astype(np.float32)
+
+
+class Generator:
+ """RNG that produces same outputs as torch.randn(..., device='cuda') on CPU"""
+
+ def __init__(self, seed):
+ self.seed = seed
+ self.offset = 0
+
+ def randn(self, shape):
+ """Generate a sequence of n standard normal random variables using the Philox 4x32 random number generator and the Box-Muller transform."""
+
+ n = 1
+ for x in shape:
+ n *= x
+
+ counter = np.zeros((4, n), dtype=np.uint32)
+ counter[0] = self.offset
+ counter[2] = np.arange(n, dtype=np.uint32) # up to 2^32 numbers can be generated - if you want more you'd need to spill into counter[3]
+ self.offset += 1
+
+ key = np.empty(n, dtype=np.uint64)
+ key.fill(self.seed)
+ key = uint32(key)
+
+ g = philox4_32(counter, key)
+
+ return box_muller(g[0], g[1]).reshape(shape) # discard g[2] and g[3]
+
+#=======================================================================================================================
+# Monkey Patch "prepare_noise" function
+# https://github.com/shiimizu/ComfyUI_smZNodes
+import torch
+import functools
+from comfy.sample import np
+import comfy.model_management
+
+def rng_rand_source(rand_source='cpu'):
+ device = comfy.model_management.text_encoder_device()
+
+ def prepare_noise(latent_image, seed, noise_inds=None, device='cpu'):
+ """
+ creates random noise given a latent image and a seed.
+ optional arg skip can be used to skip and discard x number of noise generations for a given seed
+ """
+ generator = torch.Generator(device).manual_seed(seed)
+ if rand_source == 'nv':
+ rng = Generator(seed)
+ if noise_inds is None:
+ shape = latent_image.size()
+ if rand_source == 'nv':
+ return torch.asarray(rng.randn(shape), device=device)
+ else:
+ return torch.randn(shape, dtype=latent_image.dtype, layout=latent_image.layout, generator=generator,
+ device=device)
+
+ unique_inds, inverse = np.unique(noise_inds, return_inverse=True)
+ noises = []
+ for i in range(unique_inds[-1] + 1):
+ shape = [1] + list(latent_image.size())[1:]
+ if rand_source == 'nv':
+ noise = torch.asarray(rng.randn(shape), device=device)
+ else:
+ noise = torch.randn(shape, dtype=latent_image.dtype, layout=latent_image.layout, generator=generator,
+ device=device)
+ if i in unique_inds:
+ noises.append(noise)
+ noises = [noises[i] for i in inverse]
+ noises = torch.cat(noises, axis=0)
+ return noises
+
+ if rand_source == 'cpu':
+ if hasattr(comfy.sample, 'prepare_noise_orig'):
+ comfy.sample.prepare_noise = comfy.sample.prepare_noise_orig
+ else:
+ if not hasattr(comfy.sample, 'prepare_noise_orig'):
+ comfy.sample.prepare_noise_orig = comfy.sample.prepare_noise
+ _prepare_noise = functools.partial(prepare_noise, device=device)
+ comfy.sample.prepare_noise = _prepare_noise
+
+
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/ttl_nn_latent_upscaler.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/ttl_nn_latent_upscaler.py
new file mode 100644
index 0000000000000000000000000000000000000000..7430228197ef6514244e34c64b981bfdad649791
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/py/ttl_nn_latent_upscaler.py
@@ -0,0 +1,321 @@
+import torch
+#from .latent_resizer import LatentResizer
+from comfy import model_management
+import os
+
+import torch.nn as nn
+import torch.nn.functional as F
+from einops import rearrange
+
+def normalization(channels):
+ return nn.GroupNorm(32, channels)
+
+
+def zero_module(module):
+ for p in module.parameters():
+ p.detach().zero_()
+ return module
+
+
+class AttnBlock(nn.Module):
+ def __init__(self, in_channels):
+ super().__init__()
+ self.in_channels = in_channels
+
+ self.norm = normalization(in_channels)
+ self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
+ self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
+ self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
+ self.proj_out = nn.Conv2d(
+ in_channels, in_channels, kernel_size=1, stride=1, padding=0
+ )
+
+ def attention(self, h_: torch.Tensor) -> torch.Tensor:
+ h_ = self.norm(h_)
+ q = self.q(h_)
+ k = self.k(h_)
+ v = self.v(h_)
+
+ b, c, h, w = q.shape
+ q, k, v = map(
+ lambda x: rearrange(x, "b c h w -> b 1 (h w) c").contiguous(), (q, k, v)
+ )
+ h_ = nn.functional.scaled_dot_product_attention(
+ q, k, v
+ ) # scale is dim ** -0.5 per default
+
+ return rearrange(h_, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b)
+
+ def forward(self, x, **kwargs):
+ h_ = x
+ h_ = self.attention(h_)
+ h_ = self.proj_out(h_)
+ return x + h_
+
+
+def make_attn(in_channels, attn_kwargs=None):
+ return AttnBlock(in_channels)
+
+
+class ResBlockEmb(nn.Module):
+ def __init__(
+ self,
+ channels,
+ emb_channels,
+ dropout=0,
+ out_channels=None,
+ use_conv=False,
+ use_scale_shift_norm=False,
+ kernel_size=3,
+ exchange_temb_dims=False,
+ skip_t_emb=False,
+ ):
+ super().__init__()
+ self.channels = channels
+ self.emb_channels = emb_channels
+ self.dropout = dropout
+ self.out_channels = out_channels or channels
+ self.use_conv = use_conv
+ self.use_scale_shift_norm = use_scale_shift_norm
+ self.exchange_temb_dims = exchange_temb_dims
+
+ padding = kernel_size // 2
+
+ self.in_layers = nn.Sequential(
+ normalization(channels),
+ nn.SiLU(),
+ nn.Conv2d(channels, self.out_channels, kernel_size, padding=padding),
+ )
+
+ self.skip_t_emb = skip_t_emb
+ self.emb_out_channels = (
+ 2 * self.out_channels if use_scale_shift_norm else self.out_channels
+ )
+ if self.skip_t_emb:
+ print(f"Skipping timestep embedding in {self.__class__.__name__}")
+ assert not self.use_scale_shift_norm
+ self.emb_layers = None
+ self.exchange_temb_dims = False
+ else:
+ self.emb_layers = nn.Sequential(
+ nn.SiLU(),
+ nn.Linear(
+ emb_channels,
+ self.emb_out_channels,
+ ),
+ )
+
+ self.out_layers = nn.Sequential(
+ normalization(self.out_channels),
+ nn.SiLU(),
+ nn.Dropout(p=dropout),
+ zero_module(
+ nn.Conv2d(
+ self.out_channels,
+ self.out_channels,
+ kernel_size,
+ padding=padding,
+ )
+ ),
+ )
+
+ if self.out_channels == channels:
+ self.skip_connection = nn.Identity()
+ elif use_conv:
+ self.skip_connection = nn.Conv2d(
+ channels, self.out_channels, kernel_size, padding=padding
+ )
+ else:
+ self.skip_connection = nn.Conv2d(channels, self.out_channels, 1)
+
+ def forward(self, x, emb):
+ h = self.in_layers(x)
+
+ if self.skip_t_emb:
+ emb_out = torch.zeros_like(h)
+ else:
+ emb_out = self.emb_layers(emb).type(h.dtype)
+ while len(emb_out.shape) < len(h.shape):
+ emb_out = emb_out[..., None]
+ if self.use_scale_shift_norm:
+ out_norm, out_rest = self.out_layers[0], self.out_layers[1:]
+ scale, shift = torch.chunk(emb_out, 2, dim=1)
+ h = out_norm(h) * (1 + scale) + shift
+ h = out_rest(h)
+ else:
+ if self.exchange_temb_dims:
+ emb_out = rearrange(emb_out, "b t c ... -> b c t ...")
+ h = h + emb_out
+ h = self.out_layers(h)
+ return self.skip_connection(x) + h
+
+
+class LatentResizer(nn.Module):
+ def __init__(self, in_blocks=10, out_blocks=10, channels=128, dropout=0, attn=True):
+ super().__init__()
+ self.conv_in = nn.Conv2d(4, channels, 3, padding=1)
+
+ self.channels = channels
+ embed_dim = 32
+ self.embed = nn.Sequential(
+ nn.Linear(1, embed_dim),
+ nn.SiLU(),
+ nn.Linear(embed_dim, embed_dim),
+ )
+
+ self.in_blocks = nn.ModuleList([])
+ for b in range(in_blocks):
+ if (b == 1 or b == in_blocks - 1) and attn:
+ self.in_blocks.append(make_attn(channels))
+ self.in_blocks.append(ResBlockEmb(channels, embed_dim, dropout))
+
+ self.out_blocks = nn.ModuleList([])
+ for b in range(out_blocks):
+ if (b == 1 or b == out_blocks - 1) and attn:
+ self.out_blocks.append(make_attn(channels))
+ self.out_blocks.append(ResBlockEmb(channels, embed_dim, dropout))
+
+ self.norm_out = normalization(channels)
+ self.conv_out = nn.Conv2d(channels, 4, 3, padding=1)
+
+ @classmethod
+ def load_model(cls, filename, device="cpu", dtype=torch.float32, dropout=0):
+ if not 'weights_only' in torch.load.__code__.co_varnames:
+ weights = torch.load(filename, map_location=torch.device("cpu"))
+ else:
+ weights = torch.load(filename, map_location=torch.device("cpu"), weights_only=True)
+ in_blocks = 0
+ out_blocks = 0
+ in_tfs = 0
+ out_tfs = 0
+ channels = weights["conv_in.bias"].shape[0]
+ for k in weights.keys():
+ k = k.split(".")
+ if k[0] == "in_blocks":
+ in_blocks = max(in_blocks, int(k[1]))
+ if k[2] == "q" and k[3] == "weight":
+ in_tfs += 1
+ if k[0] == "out_blocks":
+ out_blocks = max(out_blocks, int(k[1]))
+ if k[2] == "q" and k[3] == "weight":
+ out_tfs += 1
+ in_blocks = in_blocks + 1 - in_tfs
+ out_blocks = out_blocks + 1 - out_tfs
+ resizer = cls(
+ in_blocks=in_blocks,
+ out_blocks=out_blocks,
+ channels=channels,
+ dropout=dropout,
+ attn=(out_tfs != 0),
+ )
+ resizer.load_state_dict(weights)
+ resizer.eval()
+ resizer.to(device, dtype=dtype)
+ return resizer
+
+ def forward(self, x, scale=None, size=None):
+ if scale is None and size is None:
+ raise ValueError("Either scale or size needs to be not None")
+ if scale is not None and size is not None:
+ raise ValueError("Both scale or size can't be not None")
+ if scale is not None:
+ size = (x.shape[-2] * scale, x.shape[-1] * scale)
+ size = tuple([int(round(i)) for i in size])
+ else:
+ scale = size[-1] / x.shape[-1]
+
+ # Output is the same size as input
+ if size == x.shape[-2:]:
+ return x
+
+ scale = torch.tensor([scale - 1], dtype=x.dtype).to(x.device).unsqueeze(0)
+ emb = self.embed(scale)
+
+ x = self.conv_in(x)
+
+ for b in self.in_blocks:
+ if isinstance(b, ResBlockEmb):
+ x = b(x, emb)
+ else:
+ x = b(x)
+ x = F.interpolate(x, size=size, mode="bilinear")
+ for b in self.out_blocks:
+ if isinstance(b, ResBlockEmb):
+ x = b(x, emb)
+ else:
+ x = b(x)
+
+ x = self.norm_out(x)
+ x = F.silu(x)
+ x = self.conv_out(x)
+ return x
+
+########################################################
+class NNLatentUpscale:
+ """
+ Upscales SDXL latent using neural network
+ """
+
+ def __init__(self):
+ self.local_dir = os.path.dirname(os.path.realpath(__file__))
+ self.scale_factor = 0.13025
+ self.dtype = torch.float32
+ if model_management.should_use_fp16():
+ self.dtype = torch.float16
+ self.weight_path = {
+ "SDXL": os.path.join(self.local_dir, "sdxl_resizer.pt"),
+ "SD 1.x": os.path.join(self.local_dir, "sd15_resizer.pt"),
+ }
+ self.version = "none"
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "latent": ("LATENT",),
+ "version": (["SDXL", "SD 1.x"],),
+ "upscale": (
+ "FLOAT",
+ {
+ "default": 1.5,
+ "min": 1.0,
+ "max": 2.0,
+ "step": 0.01,
+ "display": "number",
+ },
+ ),
+ },
+ }
+
+ RETURN_TYPES = ("LATENT",)
+
+ FUNCTION = "upscale"
+
+ CATEGORY = "latent"
+
+ def upscale(self, latent, version, upscale):
+ device = model_management.get_torch_device()
+ samples = latent["samples"].to(device=device, dtype=self.dtype)
+
+ if version != self.version:
+ self.model = LatentResizer.load_model(self.weight_path[version], device, self.dtype)
+ self.version = version
+
+ self.model.to(device=device)
+ latent_out = (self.model(self.scale_factor * samples, scale=upscale) / self.scale_factor)
+
+ if self.dtype != torch.float32:
+ latent_out = latent_out.to(dtype=torch.float32)
+
+ latent_out = latent_out.to(device="cpu")
+
+ self.model.to(device=model_management.vae_offload_device())
+ return ({"samples": latent_out},)
+
+NODE_CLASS_MAPPINGS = {
+ "NNLatentUpscale": NNLatentUpscale
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "NNlLatentUpscale": "EFF Latent Upscale"
+}
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/requirements.txt b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..91a003faad5ae84019184079c17fb73b8bba8485
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/requirements.txt
@@ -0,0 +1,2 @@
+clip-interrogator
+simpleeval
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/tsc_utils.py b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/tsc_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..426e10eebe6ce858f35ec6a873f098e304a74817
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/tsc_utils.py
@@ -0,0 +1,555 @@
+# Efficiency Nodes Utility functions
+from torch import Tensor
+import torch
+from PIL import Image
+import numpy as np
+import os
+import sys
+import io
+from contextlib import contextmanager
+import json
+import folder_paths
+
+# Get the absolute path of the parent directory of the current script
+my_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Add the My directory path to the sys.path list
+sys.path.append(my_dir)
+
+# Construct the absolute path to the ComfyUI directory
+comfy_dir = os.path.abspath(os.path.join(my_dir, '..', '..'))
+
+# Add the ComfyUI directory path to the sys.path list
+sys.path.append(comfy_dir)
+
+# Import functions from ComfyUI
+import comfy.sd
+import comfy.utils
+import latent_preview
+from comfy.cli_args import args
+
+# Cache for Efficiency Node models
+loaded_objects = {
+ "ckpt": [], # (ckpt_name, ckpt_model, clip, bvae, [id])
+ "refn": [], # (ckpt_name, ckpt_model, clip, bvae, [id])
+ "vae": [], # (vae_name, vae, [id])
+ "lora": [] # ([(lora_name, strength_model, strength_clip)], ckpt_name, lora_model, clip_lora, [id])
+}
+
+# Cache for Efficient Ksamplers
+last_helds = {
+ "latent": [], # (latent, [parameters], id) # Base sampling latent results
+ "image": [], # (image, id) # Base sampling image results
+ "cnet_img": [] # (cnet_img, [parameters], id) # HiRes-Fix control net preprocessor image results
+}
+
+def load_ksampler_results(key: str, my_unique_id, parameters_list=None):
+ global last_helds
+ for data in last_helds[key]:
+ id_ = data[-1] # ID is always the last element in the tuple
+ if id_ == my_unique_id:
+ if parameters_list is not None:
+ # Ensure tuple has at least 3 elements and match with parameters_list
+ if len(data) >= 3 and data[1] == parameters_list:
+ return data[0]
+ else:
+ return data[0]
+ return None
+
+def store_ksampler_results(key: str, my_unique_id, value, parameters_list=None):
+ global last_helds
+
+ for i, data in enumerate(last_helds[key]):
+ id_ = data[-1] # ID will always be the last in the tuple
+ if id_ == my_unique_id:
+ # Check if parameters_list is provided or not
+ updated_data = (value, parameters_list, id_) if parameters_list is not None else (value, id_)
+ last_helds[key][i] = updated_data
+ return True
+
+ # If parameters_list is given
+ if parameters_list is not None:
+ last_helds[key].append((value, parameters_list, my_unique_id))
+ else:
+ last_helds[key].append((value, my_unique_id))
+ return True
+
+# Tensor to PIL (grabbed from WAS Suite)
+def tensor2pil(image: torch.Tensor) -> Image.Image:
+ return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
+
+# Convert PIL to Tensor (grabbed from WAS Suite)
+def pil2tensor(image: Image.Image) -> torch.Tensor:
+ return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
+
+# Convert tensor to PIL, resize it, and convert back to tensor
+def quick_resize(source_tensor: torch.Tensor, target_shape: tuple) -> torch.Tensor:
+ resized_images = []
+ for img in source_tensor:
+ resized_pil = tensor2pil(img.squeeze(0)).resize((target_shape[2], target_shape[1]), Image.ANTIALIAS)
+ resized_images.append(pil2tensor(resized_pil).squeeze(0))
+ return torch.stack(resized_images, dim=0)
+
+# Create a function to compute the hash of a tensor
+import hashlib
+def tensor_to_hash(tensor):
+ byte_repr = tensor.cpu().numpy().tobytes() # Convert tensor to bytes
+ return hashlib.sha256(byte_repr).hexdigest() # Compute hash
+
+# Color coded messages functions
+MESSAGE_COLOR = "\033[36m" # Cyan
+XYPLOT_COLOR = "\033[35m" # Purple
+SUCCESS_COLOR = "\033[92m" # Green
+WARNING_COLOR = "\033[93m" # Yellow
+ERROR_COLOR = "\033[91m" # Red
+INFO_COLOR = "\033[90m" # Gray
+def format_message(text, color_code):
+ RESET_COLOR = "\033[0m"
+ return f"{color_code}{text}{RESET_COLOR}"
+def message(text):
+ return format_message(text, MESSAGE_COLOR)
+def warning(text):
+ return format_message(text, WARNING_COLOR)
+def error(text):
+ return format_message(text, ERROR_COLOR)
+def success(text):
+ return format_message(text, SUCCESS_COLOR)
+def xyplot_message(text):
+ return format_message(text, XYPLOT_COLOR)
+def info(text):
+ return format_message(text, INFO_COLOR)
+
+def extract_node_info(prompt, id, indirect_key=None):
+ # Convert ID to string
+ id = str(id)
+ node_id = None
+
+ # If an indirect_key (like 'script') is provided, perform a two-step lookup
+ if indirect_key:
+ # Ensure the id exists in the prompt and has an 'inputs' entry with the indirect_key
+ if id in prompt and 'inputs' in prompt[id] and indirect_key in prompt[id]['inputs']:
+ # Extract the indirect_id
+ indirect_id = prompt[id]['inputs'][indirect_key][0]
+
+ # Ensure the indirect_id exists in the prompt
+ if indirect_id in prompt:
+ node_id = indirect_id
+ return prompt[indirect_id].get('class_type', None), node_id
+
+ # If indirect_key is not found within the prompt
+ return None, None
+
+ # If no indirect_key is provided, perform a direct lookup
+ return prompt.get(id, {}).get('class_type', None), node_id
+
+def extract_node_value(prompt, id, key):
+ # If ID is in data, return its 'inputs' value for a given key. Otherwise, return None.
+ return prompt.get(str(id), {}).get('inputs', {}).get(key, None)
+
+def print_loaded_objects_entries(id=None, prompt=None, show_id=False):
+ print("-" * 40) # Print an empty line followed by a separator line
+ if id is not None:
+ id = str(id) # Convert ID to string
+ if prompt is not None and id is not None:
+ node_name, _ = extract_node_info(prompt, id)
+ if show_id:
+ print(f"\033[36m{node_name} Models Cache: (node_id:{int(id)})\033[0m")
+ else:
+ print(f"\033[36m{node_name} Models Cache:\033[0m")
+ elif id is None:
+ print(f"\033[36mGlobal Models Cache:\033[0m")
+ else:
+ print(f"\033[36mModels Cache: \nnode_id:{int(id)}\033[0m")
+ entries_found = False
+ for key in ["ckpt", "refn", "vae", "lora"]:
+ entries_with_id = loaded_objects[key] if id is None else [entry for entry in loaded_objects[key] if id in entry[-1]]
+ if not entries_with_id: # If no entries with the chosen ID, print None and skip this key
+ continue
+ entries_found = True
+ print(f"{key.capitalize()}:")
+ for i, entry in enumerate(entries_with_id, 1): # Start numbering from 1
+ if key == "lora":
+ base_ckpt_name = os.path.splitext(os.path.basename(entry[1]))[0] # Split logic for base_ckpt
+ if id is None:
+ associated_ids = ', '.join(map(str, entry[-1])) # Gather all associated ids
+ print(f" [{i}] base_ckpt: {base_ckpt_name} (ids: {associated_ids})")
+ else:
+ print(f" [{i}] base_ckpt: {base_ckpt_name}")
+ for name, strength_model, strength_clip in entry[0]:
+ lora_model_info = f"{os.path.splitext(os.path.basename(name))[0]}({round(strength_model, 2)},{round(strength_clip, 2)})"
+ print(f" lora(mod,clip): {lora_model_info}")
+ else:
+ name_without_ext = os.path.splitext(os.path.basename(entry[0]))[0]
+ if id is None:
+ associated_ids = ', '.join(map(str, entry[-1])) # Gather all associated ids
+ print(f" [{i}] {name_without_ext} (ids: {associated_ids})")
+ else:
+ print(f" [{i}] {name_without_ext}")
+ if not entries_found:
+ print("-")
+
+# This function cleans global variables associated with nodes that are no longer detected on UI
+def globals_cleanup(prompt):
+ global loaded_objects
+ global last_helds
+
+ # Step 1: Clean up last_helds
+ for key in list(last_helds.keys()):
+ original_length = len(last_helds[key])
+ last_helds[key] = [
+ (*values, id_)
+ for *values, id_ in last_helds[key]
+ if str(id_) in prompt.keys()
+ ]
+
+ # Step 2: Clean up loaded_objects
+ for key in list(loaded_objects.keys()):
+ for i, tup in enumerate(list(loaded_objects[key])):
+ # Remove ids from id array in each tuple that don't exist in prompt
+ id_array = [id for id in tup[-1] if str(id) in prompt.keys()]
+ if len(id_array) != len(tup[-1]):
+ if id_array:
+ loaded_objects[key][i] = tup[:-1] + (id_array,)
+ ###print(f'Updated tuple at index {i} in {key} in loaded_objects: {loaded_objects[key][i]}')
+ else:
+ # If id array becomes empty, delete the corresponding tuple
+ loaded_objects[key].remove(tup)
+ ###print(f'Deleted tuple at index {i} in {key} in loaded_objects because its id array became empty.')
+
+def load_checkpoint(ckpt_name, id, output_vae=True, cache=None, cache_overwrite=True, ckpt_type="ckpt"):
+ global loaded_objects
+
+ # Create copies of the arguments right at the start
+ ckpt_name = ckpt_name.copy() if isinstance(ckpt_name, (list, dict, set)) else ckpt_name
+
+ # Check if the type is valid
+ if ckpt_type not in ["ckpt", "refn"]:
+ raise ValueError(f"Invalid checkpoint type: {ckpt_type}")
+
+ for entry in loaded_objects[ckpt_type]:
+ if entry[0] == ckpt_name:
+ _, model, clip, vae, ids = entry
+ cache_full = cache and len([entry for entry in loaded_objects[ckpt_type] if id in entry[-1]]) >= cache
+
+ if cache_full:
+ clear_cache(id, cache, ckpt_type)
+ elif id not in ids:
+ ids.append(id)
+
+ return model, clip, vae
+
+ if os.path.isabs(ckpt_name):
+ ckpt_path = ckpt_name
+ else:
+ ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
+ with suppress_output():
+ out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
+
+ model = out[0]
+ clip = out[1]
+ vae = out[2] if output_vae else None # Load VAE from the checkpoint path only if output_vae is True
+
+ if cache:
+ cache_list = [entry for entry in loaded_objects[ckpt_type] if id in entry[-1]]
+ if len(cache_list) < cache:
+ loaded_objects[ckpt_type].append((ckpt_name, model, clip, vae, [id]))
+ else:
+ clear_cache(id, cache, ckpt_type)
+ if cache_overwrite:
+ for e in loaded_objects[ckpt_type]:
+ if id in e[-1]:
+ e[-1].remove(id)
+ # If the id list becomes empty, remove the entry from the ckpt_type list
+ if not e[-1]:
+ loaded_objects[ckpt_type].remove(e)
+ break
+ loaded_objects[ckpt_type].append((ckpt_name, model, clip, vae, [id]))
+
+ return model, clip, vae
+
+def get_bvae_by_ckpt_name(ckpt_name):
+ for ckpt in loaded_objects["ckpt"]:
+ if ckpt[0] == ckpt_name:
+ return ckpt[3] # return 'bvae' variable
+ return None # return None if no match is found
+
+def load_vae(vae_name, id, cache=None, cache_overwrite=False):
+ global loaded_objects
+
+ # Create copies of the argument right at the start
+ vae_name = vae_name.copy() if isinstance(vae_name, (list, dict, set)) else vae_name
+
+ for i, entry in enumerate(loaded_objects["vae"]):
+ if entry[0] == vae_name:
+ vae, ids = entry[1], entry[2]
+ if id not in ids:
+ if cache and len([entry for entry in loaded_objects["vae"] if id in entry[-1]]) >= cache:
+ return vae
+ ids.append(id)
+ if cache:
+ clear_cache(id, cache, "vae")
+ return vae
+
+ if os.path.isabs(vae_name):
+ vae_path = vae_name
+ else:
+ vae_path = folder_paths.get_full_path("vae", vae_name)
+
+ sd = comfy.utils.load_torch_file(vae_path)
+ vae = comfy.sd.VAE(sd=sd)
+
+ if cache:
+ if len([entry for entry in loaded_objects["vae"] if id in entry[-1]]) < cache:
+ loaded_objects["vae"].append((vae_name, vae, [id]))
+ else:
+ clear_cache(id, cache, "vae")
+ if cache_overwrite:
+ # Find the first entry with the id, remove the id from the entry's id list
+ for e in loaded_objects["vae"]:
+ if id in e[-1]:
+ e[-1].remove(id)
+ # If the id list becomes empty, remove the entry from the "vae" list
+ if not e[-1]:
+ loaded_objects["vae"].remove(e)
+ break
+ loaded_objects["vae"].append((vae_name, vae, [id]))
+
+ return vae
+
+def load_lora(lora_params, ckpt_name, id, cache=None, ckpt_cache=None, cache_overwrite=False):
+ global loaded_objects
+
+ # Create copies of the arguments right at the start
+ lora_params = lora_params.copy() if isinstance(lora_params, (list, dict, set)) else lora_params
+ ckpt_name = ckpt_name.copy() if isinstance(ckpt_name, (list, dict, set)) else ckpt_name
+
+ for entry in loaded_objects["lora"]:
+
+ # Convert to sets and compare
+ if set(entry[0]) == set(lora_params) and entry[1] == ckpt_name:
+
+ _, _, lora_model, lora_clip, ids = entry
+ cache_full = cache and len([entry for entry in loaded_objects["lora"] if id in entry[-1]]) >= cache
+
+ if cache_full:
+ clear_cache(id, cache, "lora")
+ elif id not in ids:
+ ids.append(id)
+
+ # Additional cache handling for 'ckpt' just like in 'load_checkpoint' function
+ for ckpt_entry in loaded_objects["ckpt"]:
+ if ckpt_entry[0] == ckpt_name:
+ _, _, _, _, ckpt_ids = ckpt_entry
+ ckpt_cache_full = ckpt_cache and len(
+ [ckpt_entry for ckpt_entry in loaded_objects["ckpt"] if id in ckpt_entry[-1]]) >= ckpt_cache
+
+ if ckpt_cache_full:
+ clear_cache(id, ckpt_cache, "ckpt")
+ elif id not in ckpt_ids:
+ ckpt_ids.append(id)
+
+ return lora_model, lora_clip
+
+ def recursive_load_lora(lora_params, ckpt, clip, id, ckpt_cache, cache_overwrite, folder_paths):
+ if len(lora_params) == 0:
+ return ckpt, clip
+
+ lora_name, strength_model, strength_clip = lora_params[0]
+ if os.path.isabs(lora_name):
+ lora_path = lora_name
+ else:
+ lora_path = folder_paths.get_full_path("loras", lora_name)
+
+ lora_model, lora_clip = comfy.sd.load_lora_for_models(ckpt, clip, comfy.utils.load_torch_file(lora_path), strength_model, strength_clip)
+
+ # Call the function again with the new lora_model and lora_clip and the remaining tuples
+ return recursive_load_lora(lora_params[1:], lora_model, lora_clip, id, ckpt_cache, cache_overwrite, folder_paths)
+
+ # Unpack lora parameters from the first element of the list for now
+ lora_name, strength_model, strength_clip = lora_params[0]
+ ckpt, clip, _ = load_checkpoint(ckpt_name, id, cache=ckpt_cache)
+
+ lora_model, lora_clip = recursive_load_lora(lora_params, ckpt, clip, id, ckpt_cache, cache_overwrite, folder_paths)
+
+ if cache:
+ if len([entry for entry in loaded_objects["lora"] if id in entry[-1]]) < cache:
+ loaded_objects["lora"].append((lora_params, ckpt_name, lora_model, lora_clip, [id]))
+ else:
+ clear_cache(id, cache, "lora")
+ if cache_overwrite:
+ # Find the first entry with the id, remove the id from the entry's id list
+ for e in loaded_objects["lora"]:
+ if id in e[-1]:
+ e[-1].remove(id)
+ # If the id list becomes empty, remove the entry from the "lora" list
+ if not e[-1]:
+ loaded_objects["lora"].remove(e)
+ break
+ loaded_objects["lora"].append((lora_params, ckpt_name, lora_model, lora_clip, [id]))
+
+ return lora_model, lora_clip
+
+def clear_cache(id, cache, dict_name):
+ """
+ Clear the cache for a specific id in a specific dictionary.
+ If the cache limit is reached for a specific id, deletes the id from the oldest entry.
+ If the id array of the entry becomes empty, deletes the entry.
+ """
+ # Get all entries associated with the id_element
+ id_associated_entries = [entry for entry in loaded_objects[dict_name] if id in entry[-1]]
+ while len(id_associated_entries) > cache:
+ # Identify an older entry (but not necessarily the oldest) containing id
+ older_entry = id_associated_entries[0]
+ # Remove the id_element from the older entry
+ older_entry[-1].remove(id)
+ # If the id array of the older entry becomes empty after this, delete the entry
+ if not older_entry[-1]:
+ loaded_objects[dict_name].remove(older_entry)
+ # Update the id_associated_entries
+ id_associated_entries = [entry for entry in loaded_objects[dict_name] if id in entry[-1]]
+
+
+def clear_cache_by_exception(node_id, vae_dict=None, ckpt_dict=None, lora_dict=None, refn_dict=None):
+ global loaded_objects
+
+ dict_mapping = {
+ "vae_dict": "vae",
+ "ckpt_dict": "ckpt",
+ "lora_dict": "lora",
+ "refn_dict": "refn"
+ }
+
+ for arg_name, arg_val in {"vae_dict": vae_dict, "ckpt_dict": ckpt_dict, "lora_dict": lora_dict, "refn_dict": refn_dict}.items():
+ if arg_val is None:
+ continue
+
+ dict_name = dict_mapping[arg_name]
+
+ for tuple_idx, tuple_item in enumerate(loaded_objects[dict_name].copy()):
+ if arg_name == "lora_dict":
+ # Iterate over the tuples (lora_params, ckpt_name) in arg_val
+ for lora_params, ckpt_name in arg_val:
+ # Compare lists of tuples considering order inside tuples, but not order of tuples
+ if set(lora_params) == set(tuple_item[0]) and ckpt_name == tuple_item[1]:
+ break
+ else: # If no match was found in lora_dict, remove the tuple from loaded_objects
+ if node_id in tuple_item[-1]:
+ tuple_item[-1].remove(node_id)
+ if not tuple_item[-1]:
+ loaded_objects[dict_name].remove(tuple_item)
+ continue
+ elif tuple_item[0] not in arg_val: # Only remove the tuple if it's not in arg_val
+ if node_id in tuple_item[-1]:
+ tuple_item[-1].remove(node_id)
+ if not tuple_item[-1]:
+ loaded_objects[dict_name].remove(tuple_item)
+
+# Retrieve the cache number from 'node_settings' json file
+def get_cache_numbers(node_name):
+ # Get the directory path of the current file
+ my_dir = os.path.dirname(os.path.abspath(__file__))
+ # Construct the file path for node_settings.json
+ settings_file = os.path.join(my_dir, 'node_settings.json')
+ # Load the settings from the JSON file
+ with open(settings_file, 'r') as file:
+ node_settings = json.load(file)
+ # Retrieve the cache numbers for the given node
+ model_cache_settings = node_settings.get(node_name, {}).get('model_cache', {})
+ vae_cache = int(model_cache_settings.get('vae', 1))
+ ckpt_cache = int(model_cache_settings.get('ckpt', 1))
+ lora_cache = int(model_cache_settings.get('lora', 1))
+ refn_cache = int(model_cache_settings.get('ckpt', 1))
+ return vae_cache, ckpt_cache, lora_cache, refn_cache,
+
+def print_last_helds(id=None):
+ print("\n" + "-" * 40) # Print an empty line followed by a separator line
+ if id is not None:
+ id = str(id) # Convert ID to string
+ print(f"Node-specific Last Helds (node_id:{int(id)})")
+ else:
+ print(f"Global Last Helds:")
+ for key in ["preview_images", "latent", "output_images", "vae_decode"]:
+ entries_with_id = last_helds[key] if id is None else [entry for entry in last_helds[key] if id == entry[-1]]
+ if not entries_with_id: # If no entries with the chosen ID, print None and skip this key
+ continue
+ print(f"{key.capitalize()}:")
+ for i, entry in enumerate(entries_with_id, 1): # Start numbering from 1
+ if isinstance(entry[0], bool): # Special handling for boolean types
+ output = entry[0]
+ else:
+ output = len(entry[0])
+ if id is None:
+ print(f" [{i}] Output: {output} (id: {entry[-1]})")
+ else:
+ print(f" [{i}] Output: {output}")
+ print("-" * 40) # Print a separator line
+ print("\n") # Print an empty line
+
+# For suppressing print outputs from functions
+@contextmanager
+def suppress_output():
+ original_stdout = sys.stdout
+ original_stderr = sys.stderr
+
+ sys.stdout = io.StringIO()
+ sys.stderr = io.StringIO()
+
+ try:
+ yield
+ finally:
+ sys.stdout = original_stdout
+ sys.stderr = original_stderr
+
+# Set global preview_method
+def set_preview_method(method):
+ if method == 'auto' or method == 'LatentPreviewMethod.Auto':
+ args.preview_method = latent_preview.LatentPreviewMethod.Auto
+ elif method == 'latent2rgb' or method == 'LatentPreviewMethod.Latent2RGB':
+ args.preview_method = latent_preview.LatentPreviewMethod.Latent2RGB
+ elif method == 'taesd' or method == 'LatentPreviewMethod.TAESD':
+ args.preview_method = latent_preview.LatentPreviewMethod.TAESD
+ else:
+ args.preview_method = latent_preview.LatentPreviewMethod.NoPreviews
+
+# Extract global preview_method
+def global_preview_method():
+ return args.preview_method
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Delete efficiency nodes web extensions from 'ComfyUI\web\extensions'.
+# Pull https://github.com/comfyanonymous/ComfyUI/pull/1273 now allows defining web extensions through a dir path in init
+import shutil
+
+# Destination directory
+destination_dir = os.path.join(comfy_dir, 'web', 'extensions', 'efficiency-nodes-comfyui')
+
+# Check if the directory exists and delete it
+if os.path.exists(destination_dir):
+ shutil.rmtree(destination_dir)
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Other
+class XY_Capsule:
+ def pre_define_model(self, model, clip, vae):
+ return model, clip, vae
+
+ def set_result(self, image, latent):
+ pass
+
+ def get_result(self, model, clip, vae):
+ return None
+
+ def set_x_capsule(self, capsule):
+ return None
+
+ def getLabel(self):
+ return "Unknown"
+
+
+
+
+
+
+
+
+
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/AnimateDiff & HiResFix Scripts.gif b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/AnimateDiff & HiResFix Scripts.gif
new file mode 100644
index 0000000000000000000000000000000000000000..4662c6f50cef4fb578ee4c036a101baa5dc9f7ba
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/AnimateDiff & HiResFix Scripts.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d4332dda6b5a323359de760cb57930ad9ca613eb65632e85e93a938be14e57c8
+size 6631509
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/EFF_TiledscriptWorkflow.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/EFF_TiledscriptWorkflow.png
new file mode 100644
index 0000000000000000000000000000000000000000..6bcce552de4495e7f396a33126ee99c6091a9782
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/EFF_TiledscriptWorkflow.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Eff_XYPlot - LoRA Model vs Clip Strengths01.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Eff_XYPlot - LoRA Model vs Clip Strengths01.png
new file mode 100644
index 0000000000000000000000000000000000000000..54f3331a45a6a703b18ff9a820e808e1addeb2d1
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Eff_XYPlot - LoRA Model vs Clip Strengths01.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bd5564042d47898777555cef925d9270a8f1d8ae2c166556badc4dbdada30152
+size 1473257
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Eff_animatediff_script_wf001.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Eff_animatediff_script_wf001.png
new file mode 100644
index 0000000000000000000000000000000000000000..218eaaee6791ca74aaacbb2d04426f180bf5cb1d
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Eff_animatediff_script_wf001.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Eff_multiKsampler_withScriptsSDXL.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Eff_multiKsampler_withScriptsSDXL.png
new file mode 100644
index 0000000000000000000000000000000000000000..97fe998de0accdb3bc72d10242d0f9cf5e19a3c1
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Eff_multiKsampler_withScriptsSDXL.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fd89b5ab9501928c852d6760c20fd2debd9e4f16573fda6aeb00d2d87b0f6674
+size 6035640
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/HiResFix Script.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/HiResFix Script.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a1d160b02a3fbce3aff4bfc40840d4216eee413
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/HiResFix Script.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:13220cef9a2371592cee86c494e0c58b95a9b4e5258e9a06fdb2d73930fafff9
+size 1046496
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/HiResfix_workflow.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/HiResfix_workflow.png
new file mode 100644
index 0000000000000000000000000000000000000000..34cbd71dd1a32b17efb27f1b26543eec7bc221f8
Binary files /dev/null and b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/HiResfix_workflow.png differ
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/SDXL Refining & Noise Control Script.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/SDXL Refining & Noise Control Script.png
new file mode 100644
index 0000000000000000000000000000000000000000..944c48574366c81ebd9414f05f5ecd6ec183b7b4
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/SDXL Refining & Noise Control Script.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:020fbd729fa01793b214b315ca5f8e9102f3600199810755e360ed28fe6104fa
+size 1088960
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/SDXL_base_refine_noise_workflow.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/SDXL_base_refine_noise_workflow.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f45e4807d41e439a51f71f2e8938fe1e3166f87
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/SDXL_base_refine_noise_workflow.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:57070784efece2cf03396e4512d7723cf0384fe5d8f3ba25ad7d329f33f5a6d6
+size 1731313
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/SimpleEval_Node_Examples.txt b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/SimpleEval_Node_Examples.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2ac0307880c8f79a67b96c2b642eaf3cf39c25d2
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/SimpleEval_Node_Examples.txt
@@ -0,0 +1,56 @@
+The Evaluate Integers, Floats, and Strings nodes
+now employ the SimpleEval library, enabling secure
+creation and execution of custom Python expressions.
+
+(https://github.com/danthedeckie/simpleeval)
+
+Below is a short list of what is possible.
+______________________________________________
+
+"EVALUATE INTEGERS/FLOATS" NODE EXPRESSION EXAMPLES:
+
+Addition: a + b + c
+Subtraction: a - b - c
+Multiplication: a * b * c
+Division: a / b / c
+Modulo: a % b % c
+Exponentiation: a ** b ** c
+Floor Division: a // b // c
+Absolute Value: abs(a) + abs(b) + abs(c)
+Maximum: max(a, b, c)
+Minimum: min(a, b, c)
+Sum of Squares: a**2 + b**2 + c**2
+Bitwise And: a & b & c
+Bitwise Or: a | b | c
+Bitwise Xor: a ^ b ^ c
+Left Shift: a << 1 + b << 1 + c << 1
+Right Shift: a >> 1 + b >> 1 + c >> 1
+Greater Than Comparison: a > b > c
+Less Than Comparison: a < b < c
+Equal To Comparison: a == b == c
+Not Equal To Comparison: a != b != c
+______________________________________________
+
+"EVALUATE STRINGS" NODE EXPRESSION EXAMPLES:
+
+Concatenate: a + b + c
+Format: f'{a} {b} {c}'
+Length: len(a) + len(b) + len(c)
+Uppercase: a.upper() + b.upper() + c.upper()
+Lowercase: a.lower() + b.lower() + c.lower()
+Capitalize: a.capitalize() + b.capitalize() + c.capitalize()
+Title Case: a.title() + b.title() + c.title()
+Strip: a.strip() + b.strip() + c.strip()
+Find Substring: a.find('sub') + b.find('sub') + c.find('sub')
+Replace Substring: a.replace('old', 'new') + b.replace('old', 'new') + c.replace('old', 'new')
+Count Substring: a.count('sub') + b.count('sub') + c.count('sub')
+Check Numeric: a.isnumeric() + b.isnumeric() + c.isnumeric()
+Check Alphabetic: a.isalpha() + b.isalpha() + c.isalpha()
+Check Alphanumeric: a.isalnum() + b.isalnum() + c.isalnum()
+Check Start: a.startswith('prefix') + b.startswith('prefix') + c.startswith('prefix')
+Check End: a.endswith('suffix') + b.endswith('suffix') + c.endswith('suffix')
+Split: a.split(' ') + b.split(' ') + c.split(' ')
+Zero Fill: a.zfill(5) + b.zfill(5) + c.zfill(5)
+Slice: a[:5] + b[:5] + c[:5]
+Reverse: a[::-1] + b[::-1] + c[::-1]
+______________________________________________
\ No newline at end of file
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Tiled Upscaler Script.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Tiled Upscaler Script.png
new file mode 100644
index 0000000000000000000000000000000000000000..51d62203044f83dc32474e024764fcf03e39d100
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/Tiled Upscaler Script.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:da8095a7c844893cc69dd1ee161147ba7d92ad0f37a8bcc96ef3bb1a7a53a7d3
+size 2391389
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/XY Plot Input Manual Entry Notes.txt b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/XY Plot Input Manual Entry Notes.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/XYPlot - LoRA Model vs Clip Strengths.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/XYPlot - LoRA Model vs Clip Strengths.png
new file mode 100644
index 0000000000000000000000000000000000000000..68e5d9c99a026eb2536042e94254629fc9492a96
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/XYPlot - LoRA Model vs Clip Strengths.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:94ac42ed71cb0da01489897c23cb3cbcac07c90891cd33c3ab2697c251f8c886
+size 1619439
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/XYPlot - Seeds vs Checkpoints & Stacked Scripts.png b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/XYPlot - Seeds vs Checkpoints & Stacked Scripts.png
new file mode 100644
index 0000000000000000000000000000000000000000..8bb72a23c39f2bf18ab06a18d6952a4a2b31e391
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/XYPlot - Seeds vs Checkpoints & Stacked Scripts.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c8b49cc06c71eee5a911740321126e976dc47fd4f356f1974b117047a4e21ab9
+size 1189821
diff --git a/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/eff_animatescriptWF001.gif b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/eff_animatescriptWF001.gif
new file mode 100644
index 0000000000000000000000000000000000000000..6f6a0e958593c047d21335949172cb3b3ee84b00
--- /dev/null
+++ b/ComfyUI/custom_nodes/efficiency-nodes-comfyui/workflows/eff_animatescriptWF001.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f8d9caa168bda753ab909e8a5da766867371d359d7e168f0e97431bf51ba42b0
+size 13015298
diff --git a/ComfyUI/custom_nodes/example_node.py.example b/ComfyUI/custom_nodes/example_node.py.example
new file mode 100644
index 0000000000000000000000000000000000000000..733014f3c7d393a81130b41810e3e1d574dd256b
--- /dev/null
+++ b/ComfyUI/custom_nodes/example_node.py.example
@@ -0,0 +1,102 @@
+class Example:
+ """
+ A example node
+
+ Class methods
+ -------------
+ INPUT_TYPES (dict):
+ Tell the main program input parameters of nodes.
+
+ Attributes
+ ----------
+ RETURN_TYPES (`tuple`):
+ The type of each element in the output tulple.
+ RETURN_NAMES (`tuple`):
+ Optional: The name of each output in the output tulple.
+ FUNCTION (`str`):
+ The name of the entry-point method. For example, if `FUNCTION = "execute"` then it will run Example().execute()
+ OUTPUT_NODE ([`bool`]):
+ If this node is an output node that outputs a result/image from the graph. The SaveImage node is an example.
+ The backend iterates on these output nodes and tries to execute all their parents if their parent graph is properly connected.
+ Assumed to be False if not present.
+ CATEGORY (`str`):
+ The category the node should appear in the UI.
+ execute(s) -> tuple || None:
+ The entry point method. The name of this method must be the same as the value of property `FUNCTION`.
+ For example, if `FUNCTION = "execute"` then this method's name must be `execute`, if `FUNCTION = "foo"` then it must be `foo`.
+ """
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ """
+ Return a dictionary which contains config for all input fields.
+ Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT".
+ Input types "INT", "STRING" or "FLOAT" are special values for fields on the node.
+ The type can be a list for selection.
+
+ Returns: `dict`:
+ - Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required`
+ - Value input_fields (`dict`): Contains input fields config:
+ * Key field_name (`string`): Name of a entry-point method's argument
+ * Value field_config (`tuple`):
+ + First value is a string indicate the type of field or a list for selection.
+ + Secound value is a config for type "INT", "STRING" or "FLOAT".
+ """
+ return {
+ "required": {
+ "image": ("IMAGE",),
+ "int_field": ("INT", {
+ "default": 0,
+ "min": 0, #Minimum value
+ "max": 4096, #Maximum value
+ "step": 64, #Slider's step
+ "display": "number" # Cosmetic only: display as "number" or "slider"
+ }),
+ "float_field": ("FLOAT", {
+ "default": 1.0,
+ "min": 0.0,
+ "max": 10.0,
+ "step": 0.01,
+ "round": 0.001, #The value represeting the precision to round to, will be set to the step value by default. Can be set to False to disable rounding.
+ "display": "number"}),
+ "print_to_screen": (["enable", "disable"],),
+ "string_field": ("STRING", {
+ "multiline": False, #True if you want the field to look like the one on the ClipTextEncode node
+ "default": "Hello World!"
+ }),
+ },
+ }
+
+ RETURN_TYPES = ("IMAGE",)
+ #RETURN_NAMES = ("image_output_name",)
+
+ FUNCTION = "test"
+
+ #OUTPUT_NODE = False
+
+ CATEGORY = "Example"
+
+ def test(self, image, string_field, int_field, float_field, print_to_screen):
+ if print_to_screen == "enable":
+ print(f"""Your input contains:
+ string_field aka input text: {string_field}
+ int_field: {int_field}
+ float_field: {float_field}
+ """)
+ #do some processing on the image, in this example I just invert it
+ image = 1.0 - image
+ return (image,)
+
+
+# A dictionary that contains all nodes you want to export with their names
+# NOTE: names should be globally unique
+NODE_CLASS_MAPPINGS = {
+ "Example": Example
+}
+
+# A dictionary that contains the friendly/humanly readable titles for the nodes
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "Example": "Example Node"
+}
diff --git a/ComfyUI/execution.py b/ComfyUI/execution.py
new file mode 100644
index 0000000000000000000000000000000000000000..53ba2e0f8a3be86d892a18a6f92453bc2f249ed4
--- /dev/null
+++ b/ComfyUI/execution.py
@@ -0,0 +1,794 @@
+import os
+import sys
+import copy
+import json
+import logging
+import threading
+import heapq
+import traceback
+import gc
+import inspect
+
+import torch
+import nodes
+
+import comfy.model_management
+
+def get_input_data(inputs, class_def, unique_id, outputs={}, prompt={}, extra_data={}):
+ valid_inputs = class_def.INPUT_TYPES()
+ input_data_all = {}
+ for x in inputs:
+ input_data = inputs[x]
+ if isinstance(input_data, list):
+ input_unique_id = input_data[0]
+ output_index = input_data[1]
+ if input_unique_id not in outputs:
+ input_data_all[x] = (None,)
+ continue
+ obj = outputs[input_unique_id][output_index]
+ input_data_all[x] = obj
+ else:
+ if ("required" in valid_inputs and x in valid_inputs["required"]) or ("optional" in valid_inputs and x in valid_inputs["optional"]):
+ input_data_all[x] = [input_data]
+
+ if "hidden" in valid_inputs:
+ h = valid_inputs["hidden"]
+ for x in h:
+ if h[x] == "PROMPT":
+ input_data_all[x] = [prompt]
+ if h[x] == "EXTRA_PNGINFO":
+ if "extra_pnginfo" in extra_data:
+ input_data_all[x] = [extra_data['extra_pnginfo']]
+ if h[x] == "UNIQUE_ID":
+ input_data_all[x] = [unique_id]
+ return input_data_all
+
+def map_node_over_list(obj, input_data_all, func, allow_interrupt=False):
+ # check if node wants the lists
+ input_is_list = False
+ if hasattr(obj, "INPUT_IS_LIST"):
+ input_is_list = obj.INPUT_IS_LIST
+
+ if len(input_data_all) == 0:
+ max_len_input = 0
+ else:
+ max_len_input = max([len(x) for x in input_data_all.values()])
+
+ # get a slice of inputs, repeat last input when list isn't long enough
+ def slice_dict(d, i):
+ d_new = dict()
+ for k,v in d.items():
+ d_new[k] = v[i if len(v) > i else -1]
+ return d_new
+
+ results = []
+ if input_is_list:
+ if allow_interrupt:
+ nodes.before_node_execution()
+ results.append(getattr(obj, func)(**input_data_all))
+ elif max_len_input == 0:
+ if allow_interrupt:
+ nodes.before_node_execution()
+ results.append(getattr(obj, func)())
+ else:
+ for i in range(max_len_input):
+ if allow_interrupt:
+ nodes.before_node_execution()
+ results.append(getattr(obj, func)(**slice_dict(input_data_all, i)))
+ return results
+
+def get_output_data(obj, input_data_all):
+
+ results = []
+ uis = []
+ return_values = map_node_over_list(obj, input_data_all, obj.FUNCTION, allow_interrupt=True)
+
+ for r in return_values:
+ if isinstance(r, dict):
+ if 'ui' in r:
+ uis.append(r['ui'])
+ if 'result' in r:
+ results.append(r['result'])
+ else:
+ results.append(r)
+
+ output = []
+ if len(results) > 0:
+ # check which outputs need concatenating
+ output_is_list = [False] * len(results[0])
+ if hasattr(obj, "OUTPUT_IS_LIST"):
+ output_is_list = obj.OUTPUT_IS_LIST
+
+ # merge node execution results
+ for i, is_list in zip(range(len(results[0])), output_is_list):
+ if is_list:
+ output.append([x for o in results for x in o[i]])
+ else:
+ output.append([o[i] for o in results])
+
+ ui = dict()
+ if len(uis) > 0:
+ ui = {k: [y for x in uis for y in x[k]] for k in uis[0].keys()}
+ return output, ui
+
+def format_value(x):
+ if x is None:
+ return None
+ elif isinstance(x, (int, float, bool, str)):
+ return x
+ else:
+ return str(x)
+
+def recursive_execute(server, prompt, outputs, current_item, extra_data, executed, prompt_id, outputs_ui, object_storage):
+ unique_id = current_item
+ inputs = prompt[unique_id]['inputs']
+ class_type = prompt[unique_id]['class_type']
+ class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
+ if unique_id in outputs:
+ return (True, None, None)
+
+ for x in inputs:
+ input_data = inputs[x]
+
+ if isinstance(input_data, list):
+ input_unique_id = input_data[0]
+ output_index = input_data[1]
+ if input_unique_id not in outputs:
+ result = recursive_execute(server, prompt, outputs, input_unique_id, extra_data, executed, prompt_id, outputs_ui, object_storage)
+ if result[0] is not True:
+ # Another node failed further upstream
+ return result
+
+ input_data_all = None
+ try:
+ input_data_all = get_input_data(inputs, class_def, unique_id, outputs, prompt, extra_data)
+ if server.client_id is not None:
+ server.last_node_id = unique_id
+ server.send_sync("executing", { "node": unique_id, "prompt_id": prompt_id }, server.client_id)
+
+ obj = object_storage.get((unique_id, class_type), None)
+ if obj is None:
+ obj = class_def()
+ object_storage[(unique_id, class_type)] = obj
+
+ output_data, output_ui = get_output_data(obj, input_data_all)
+ outputs[unique_id] = output_data
+ if len(output_ui) > 0:
+ outputs_ui[unique_id] = output_ui
+ if server.client_id is not None:
+ server.send_sync("executed", { "node": unique_id, "output": output_ui, "prompt_id": prompt_id }, server.client_id)
+ except comfy.model_management.InterruptProcessingException as iex:
+ logging.info("Processing interrupted")
+
+ # skip formatting inputs/outputs
+ error_details = {
+ "node_id": unique_id,
+ }
+
+ return (False, error_details, iex)
+ except Exception as ex:
+ typ, _, tb = sys.exc_info()
+ exception_type = full_type_name(typ)
+ input_data_formatted = {}
+ if input_data_all is not None:
+ input_data_formatted = {}
+ for name, inputs in input_data_all.items():
+ input_data_formatted[name] = [format_value(x) for x in inputs]
+
+ output_data_formatted = {}
+ for node_id, node_outputs in outputs.items():
+ output_data_formatted[node_id] = [[format_value(x) for x in l] for l in node_outputs]
+
+ logging.error("!!! Exception during processing !!!")
+ logging.error(traceback.format_exc())
+
+ error_details = {
+ "node_id": unique_id,
+ "exception_message": str(ex),
+ "exception_type": exception_type,
+ "traceback": traceback.format_tb(tb),
+ "current_inputs": input_data_formatted,
+ "current_outputs": output_data_formatted
+ }
+ return (False, error_details, ex)
+
+ executed.add(unique_id)
+
+ return (True, None, None)
+
+def recursive_will_execute(prompt, outputs, current_item):
+ unique_id = current_item
+ inputs = prompt[unique_id]['inputs']
+ will_execute = []
+ if unique_id in outputs:
+ return []
+
+ for x in inputs:
+ input_data = inputs[x]
+ if isinstance(input_data, list):
+ input_unique_id = input_data[0]
+ output_index = input_data[1]
+ if input_unique_id not in outputs:
+ will_execute += recursive_will_execute(prompt, outputs, input_unique_id)
+
+ return will_execute + [unique_id]
+
+def recursive_output_delete_if_changed(prompt, old_prompt, outputs, current_item):
+ unique_id = current_item
+ inputs = prompt[unique_id]['inputs']
+ class_type = prompt[unique_id]['class_type']
+ class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
+
+ is_changed_old = ''
+ is_changed = ''
+ to_delete = False
+ if hasattr(class_def, 'IS_CHANGED'):
+ if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]:
+ is_changed_old = old_prompt[unique_id]['is_changed']
+ if 'is_changed' not in prompt[unique_id]:
+ input_data_all = get_input_data(inputs, class_def, unique_id, outputs)
+ if input_data_all is not None:
+ try:
+ #is_changed = class_def.IS_CHANGED(**input_data_all)
+ is_changed = map_node_over_list(class_def, input_data_all, "IS_CHANGED")
+ prompt[unique_id]['is_changed'] = is_changed
+ except:
+ to_delete = True
+ else:
+ is_changed = prompt[unique_id]['is_changed']
+
+ if unique_id not in outputs:
+ return True
+
+ if not to_delete:
+ if is_changed != is_changed_old:
+ to_delete = True
+ elif unique_id not in old_prompt:
+ to_delete = True
+ elif inputs == old_prompt[unique_id]['inputs']:
+ for x in inputs:
+ input_data = inputs[x]
+
+ if isinstance(input_data, list):
+ input_unique_id = input_data[0]
+ output_index = input_data[1]
+ if input_unique_id in outputs:
+ to_delete = recursive_output_delete_if_changed(prompt, old_prompt, outputs, input_unique_id)
+ else:
+ to_delete = True
+ if to_delete:
+ break
+ else:
+ to_delete = True
+
+ if to_delete:
+ d = outputs.pop(unique_id)
+ del d
+ return to_delete
+
+class PromptExecutor:
+ def __init__(self, server):
+ self.outputs = {}
+ self.object_storage = {}
+ self.outputs_ui = {}
+ self.old_prompt = {}
+ self.server = server
+
+ def handle_execution_error(self, prompt_id, prompt, current_outputs, executed, error, ex):
+ node_id = error["node_id"]
+ class_type = prompt[node_id]["class_type"]
+
+ # First, send back the status to the frontend depending
+ # on the exception type
+ if isinstance(ex, comfy.model_management.InterruptProcessingException):
+ mes = {
+ "prompt_id": prompt_id,
+ "node_id": node_id,
+ "node_type": class_type,
+ "executed": list(executed),
+ }
+ self.server.send_sync("execution_interrupted", mes, self.server.client_id)
+ else:
+ if self.server.client_id is not None:
+ mes = {
+ "prompt_id": prompt_id,
+ "node_id": node_id,
+ "node_type": class_type,
+ "executed": list(executed),
+
+ "exception_message": error["exception_message"],
+ "exception_type": error["exception_type"],
+ "traceback": error["traceback"],
+ "current_inputs": error["current_inputs"],
+ "current_outputs": error["current_outputs"],
+ }
+ self.server.send_sync("execution_error", mes, self.server.client_id)
+
+ # Next, remove the subsequent outputs since they will not be executed
+ to_delete = []
+ for o in self.outputs:
+ if (o not in current_outputs) and (o not in executed):
+ to_delete += [o]
+ if o in self.old_prompt:
+ d = self.old_prompt.pop(o)
+ del d
+ for o in to_delete:
+ d = self.outputs.pop(o)
+ del d
+
+ def execute(self, prompt, prompt_id, extra_data={}, execute_outputs=[]):
+ nodes.interrupt_processing(False)
+
+ if "client_id" in extra_data:
+ self.server.client_id = extra_data["client_id"]
+ else:
+ self.server.client_id = None
+
+ if self.server.client_id is not None:
+ self.server.send_sync("execution_start", { "prompt_id": prompt_id}, self.server.client_id)
+
+ with torch.inference_mode():
+ #delete cached outputs if nodes don't exist for them
+ to_delete = []
+ for o in self.outputs:
+ if o not in prompt:
+ to_delete += [o]
+ for o in to_delete:
+ d = self.outputs.pop(o)
+ del d
+ to_delete = []
+ for o in self.object_storage:
+ if o[0] not in prompt:
+ to_delete += [o]
+ else:
+ p = prompt[o[0]]
+ if o[1] != p['class_type']:
+ to_delete += [o]
+ for o in to_delete:
+ d = self.object_storage.pop(o)
+ del d
+
+ for x in prompt:
+ recursive_output_delete_if_changed(prompt, self.old_prompt, self.outputs, x)
+
+ current_outputs = set(self.outputs.keys())
+ for x in list(self.outputs_ui.keys()):
+ if x not in current_outputs:
+ d = self.outputs_ui.pop(x)
+ del d
+
+ comfy.model_management.cleanup_models()
+ if self.server.client_id is not None:
+ self.server.send_sync("execution_cached", { "nodes": list(current_outputs) , "prompt_id": prompt_id}, self.server.client_id)
+ executed = set()
+ output_node_id = None
+ to_execute = []
+
+ for node_id in list(execute_outputs):
+ to_execute += [(0, node_id)]
+
+ while len(to_execute) > 0:
+ #always execute the output that depends on the least amount of unexecuted nodes first
+ to_execute = sorted(list(map(lambda a: (len(recursive_will_execute(prompt, self.outputs, a[-1])), a[-1]), to_execute)))
+ output_node_id = to_execute.pop(0)[-1]
+
+ # This call shouldn't raise anything if there's an error deep in
+ # the actual SD code, instead it will report the node where the
+ # error was raised
+ success, error, ex = recursive_execute(self.server, prompt, self.outputs, output_node_id, extra_data, executed, prompt_id, self.outputs_ui, self.object_storage)
+ if success is not True:
+ self.handle_execution_error(prompt_id, prompt, current_outputs, executed, error, ex)
+ break
+
+ for x in executed:
+ self.old_prompt[x] = copy.deepcopy(prompt[x])
+ self.server.last_node_id = None
+ if comfy.model_management.DISABLE_SMART_MEMORY:
+ comfy.model_management.unload_all_models()
+
+
+
+def validate_inputs(prompt, item, validated):
+ unique_id = item
+ if unique_id in validated:
+ return validated[unique_id]
+
+ inputs = prompt[unique_id]['inputs']
+ class_type = prompt[unique_id]['class_type']
+ obj_class = nodes.NODE_CLASS_MAPPINGS[class_type]
+
+ class_inputs = obj_class.INPUT_TYPES()
+ required_inputs = class_inputs['required']
+
+ errors = []
+ valid = True
+
+ validate_function_inputs = []
+ if hasattr(obj_class, "VALIDATE_INPUTS"):
+ validate_function_inputs = inspect.getfullargspec(obj_class.VALIDATE_INPUTS).args
+
+ for x in required_inputs:
+ if x not in inputs:
+ error = {
+ "type": "required_input_missing",
+ "message": "Required input is missing",
+ "details": f"{x}",
+ "extra_info": {
+ "input_name": x
+ }
+ }
+ errors.append(error)
+ continue
+
+ val = inputs[x]
+ info = required_inputs[x]
+ type_input = info[0]
+ if isinstance(val, list):
+ if len(val) != 2:
+ error = {
+ "type": "bad_linked_input",
+ "message": "Bad linked input, must be a length-2 list of [node_id, slot_index]",
+ "details": f"{x}",
+ "extra_info": {
+ "input_name": x,
+ "input_config": info,
+ "received_value": val
+ }
+ }
+ errors.append(error)
+ continue
+
+ o_id = val[0]
+ o_class_type = prompt[o_id]['class_type']
+ r = nodes.NODE_CLASS_MAPPINGS[o_class_type].RETURN_TYPES
+ if r[val[1]] != type_input:
+ received_type = r[val[1]]
+ details = f"{x}, {received_type} != {type_input}"
+ error = {
+ "type": "return_type_mismatch",
+ "message": "Return type mismatch between linked nodes",
+ "details": details,
+ "extra_info": {
+ "input_name": x,
+ "input_config": info,
+ "received_type": received_type,
+ "linked_node": val
+ }
+ }
+ errors.append(error)
+ continue
+ try:
+ r = validate_inputs(prompt, o_id, validated)
+ if r[0] is False:
+ # `r` will be set in `validated[o_id]` already
+ valid = False
+ continue
+ except Exception as ex:
+ typ, _, tb = sys.exc_info()
+ valid = False
+ exception_type = full_type_name(typ)
+ reasons = [{
+ "type": "exception_during_inner_validation",
+ "message": "Exception when validating inner node",
+ "details": str(ex),
+ "extra_info": {
+ "input_name": x,
+ "input_config": info,
+ "exception_message": str(ex),
+ "exception_type": exception_type,
+ "traceback": traceback.format_tb(tb),
+ "linked_node": val
+ }
+ }]
+ validated[o_id] = (False, reasons, o_id)
+ continue
+ else:
+ try:
+ if type_input == "INT":
+ val = int(val)
+ inputs[x] = val
+ if type_input == "FLOAT":
+ val = float(val)
+ inputs[x] = val
+ if type_input == "STRING":
+ val = str(val)
+ inputs[x] = val
+ except Exception as ex:
+ error = {
+ "type": "invalid_input_type",
+ "message": f"Failed to convert an input value to a {type_input} value",
+ "details": f"{x}, {val}, {ex}",
+ "extra_info": {
+ "input_name": x,
+ "input_config": info,
+ "received_value": val,
+ "exception_message": str(ex)
+ }
+ }
+ errors.append(error)
+ continue
+
+ if len(info) > 1:
+ if "min" in info[1] and val < info[1]["min"]:
+ error = {
+ "type": "value_smaller_than_min",
+ "message": "Value {} smaller than min of {}".format(val, info[1]["min"]),
+ "details": f"{x}",
+ "extra_info": {
+ "input_name": x,
+ "input_config": info,
+ "received_value": val,
+ }
+ }
+ errors.append(error)
+ continue
+ if "max" in info[1] and val > info[1]["max"]:
+ error = {
+ "type": "value_bigger_than_max",
+ "message": "Value {} bigger than max of {}".format(val, info[1]["max"]),
+ "details": f"{x}",
+ "extra_info": {
+ "input_name": x,
+ "input_config": info,
+ "received_value": val,
+ }
+ }
+ errors.append(error)
+ continue
+
+ if x not in validate_function_inputs:
+ if isinstance(type_input, list):
+ if val not in type_input:
+ input_config = info
+ list_info = ""
+
+ # Don't send back gigantic lists like if they're lots of
+ # scanned model filepaths
+ if len(type_input) > 20:
+ list_info = f"(list of length {len(type_input)})"
+ input_config = None
+ else:
+ list_info = str(type_input)
+
+ error = {
+ "type": "value_not_in_list",
+ "message": "Value not in list",
+ "details": f"{x}: '{val}' not in {list_info}",
+ "extra_info": {
+ "input_name": x,
+ "input_config": input_config,
+ "received_value": val,
+ }
+ }
+ errors.append(error)
+ continue
+
+ if len(validate_function_inputs) > 0:
+ input_data_all = get_input_data(inputs, obj_class, unique_id)
+ input_filtered = {}
+ for x in input_data_all:
+ if x in validate_function_inputs:
+ input_filtered[x] = input_data_all[x]
+
+ #ret = obj_class.VALIDATE_INPUTS(**input_filtered)
+ ret = map_node_over_list(obj_class, input_filtered, "VALIDATE_INPUTS")
+ for x in input_filtered:
+ for i, r in enumerate(ret):
+ if r is not True:
+ details = f"{x}"
+ if r is not False:
+ details += f" - {str(r)}"
+
+ error = {
+ "type": "custom_validation_failed",
+ "message": "Custom validation failed for node",
+ "details": details,
+ "extra_info": {
+ "input_name": x,
+ "input_config": info,
+ "received_value": val,
+ }
+ }
+ errors.append(error)
+ continue
+
+ if len(errors) > 0 or valid is not True:
+ ret = (False, errors, unique_id)
+ else:
+ ret = (True, [], unique_id)
+
+ validated[unique_id] = ret
+ return ret
+
+def full_type_name(klass):
+ module = klass.__module__
+ if module == 'builtins':
+ return klass.__qualname__
+ return module + '.' + klass.__qualname__
+
+def validate_prompt(prompt):
+ outputs = set()
+ for x in prompt:
+ class_ = nodes.NODE_CLASS_MAPPINGS[prompt[x]['class_type']]
+ if hasattr(class_, 'OUTPUT_NODE') and class_.OUTPUT_NODE == True:
+ outputs.add(x)
+
+ if len(outputs) == 0:
+ error = {
+ "type": "prompt_no_outputs",
+ "message": "Prompt has no outputs",
+ "details": "",
+ "extra_info": {}
+ }
+ return (False, error, [], [])
+
+ good_outputs = set()
+ errors = []
+ node_errors = {}
+ validated = {}
+ for o in outputs:
+ valid = False
+ reasons = []
+ try:
+ m = validate_inputs(prompt, o, validated)
+ valid = m[0]
+ reasons = m[1]
+ except Exception as ex:
+ typ, _, tb = sys.exc_info()
+ valid = False
+ exception_type = full_type_name(typ)
+ reasons = [{
+ "type": "exception_during_validation",
+ "message": "Exception when validating node",
+ "details": str(ex),
+ "extra_info": {
+ "exception_type": exception_type,
+ "traceback": traceback.format_tb(tb)
+ }
+ }]
+ validated[o] = (False, reasons, o)
+
+ if valid is True:
+ good_outputs.add(o)
+ else:
+ logging.error(f"Failed to validate prompt for output {o}:")
+ if len(reasons) > 0:
+ logging.error("* (prompt):")
+ for reason in reasons:
+ logging.error(f" - {reason['message']}: {reason['details']}")
+ errors += [(o, reasons)]
+ for node_id, result in validated.items():
+ valid = result[0]
+ reasons = result[1]
+ # If a node upstream has errors, the nodes downstream will also
+ # be reported as invalid, but there will be no errors attached.
+ # So don't return those nodes as having errors in the response.
+ if valid is not True and len(reasons) > 0:
+ if node_id not in node_errors:
+ class_type = prompt[node_id]['class_type']
+ node_errors[node_id] = {
+ "errors": reasons,
+ "dependent_outputs": [],
+ "class_type": class_type
+ }
+ logging.error(f"* {class_type} {node_id}:")
+ for reason in reasons:
+ logging.error(f" - {reason['message']}: {reason['details']}")
+ node_errors[node_id]["dependent_outputs"].append(o)
+ logging.error("Output will be ignored")
+
+ if len(good_outputs) == 0:
+ errors_list = []
+ for o, errors in errors:
+ for error in errors:
+ errors_list.append(f"{error['message']}: {error['details']}")
+ errors_list = "\n".join(errors_list)
+
+ error = {
+ "type": "prompt_outputs_failed_validation",
+ "message": "Prompt outputs failed validation",
+ "details": errors_list,
+ "extra_info": {}
+ }
+
+ return (False, error, list(good_outputs), node_errors)
+
+ return (True, None, list(good_outputs), node_errors)
+
+MAXIMUM_HISTORY_SIZE = 10000
+
+class PromptQueue:
+ def __init__(self, server):
+ self.server = server
+ self.mutex = threading.RLock()
+ self.not_empty = threading.Condition(self.mutex)
+ self.task_counter = 0
+ self.queue = []
+ self.currently_running = {}
+ self.history = {}
+ server.prompt_queue = self
+
+ def put(self, item):
+ with self.mutex:
+ heapq.heappush(self.queue, item)
+ self.server.queue_updated()
+ self.not_empty.notify()
+
+ def get(self, timeout=None):
+ with self.not_empty:
+ while len(self.queue) == 0:
+ self.not_empty.wait(timeout=timeout)
+ if timeout is not None and len(self.queue) == 0:
+ return None
+ item = heapq.heappop(self.queue)
+ i = self.task_counter
+ self.currently_running[i] = copy.deepcopy(item)
+ self.task_counter += 1
+ self.server.queue_updated()
+ return (item, i)
+
+ def task_done(self, item_id, outputs):
+ with self.mutex:
+ prompt = self.currently_running.pop(item_id)
+ if len(self.history) > MAXIMUM_HISTORY_SIZE:
+ self.history.pop(next(iter(self.history)))
+ self.history[prompt[1]] = { "prompt": prompt, "outputs": {} }
+ for o in outputs:
+ self.history[prompt[1]]["outputs"][o] = outputs[o]
+ self.server.queue_updated()
+
+ def get_current_queue(self):
+ with self.mutex:
+ out = []
+ for x in self.currently_running.values():
+ out += [x]
+ return (out, copy.deepcopy(self.queue))
+
+ def get_tasks_remaining(self):
+ with self.mutex:
+ return len(self.queue) + len(self.currently_running)
+
+ def wipe_queue(self):
+ with self.mutex:
+ self.queue = []
+ self.server.queue_updated()
+
+ def delete_queue_item(self, function):
+ with self.mutex:
+ for x in range(len(self.queue)):
+ if function(self.queue[x]):
+ if len(self.queue) == 1:
+ self.wipe_queue()
+ else:
+ self.queue.pop(x)
+ heapq.heapify(self.queue)
+ self.server.queue_updated()
+ return True
+ return False
+
+ def get_history(self, prompt_id=None, max_items=None, offset=-1):
+ with self.mutex:
+ if prompt_id is None:
+ out = {}
+ i = 0
+ if offset < 0 and max_items is not None:
+ offset = len(self.history) - max_items
+ for k in self.history:
+ if i >= offset:
+ out[k] = self.history[k]
+ if max_items is not None and len(out) >= max_items:
+ break
+ i += 1
+ return out
+ elif prompt_id in self.history:
+ return {prompt_id: copy.deepcopy(self.history[prompt_id])}
+ else:
+ return {}
+
+ def wipe_history(self):
+ with self.mutex:
+ self.history = {}
+
+ def delete_history_item(self, id_to_delete):
+ with self.mutex:
+ self.history.pop(id_to_delete, None)
diff --git a/ComfyUI/extra_model_paths.yaml.example b/ComfyUI/extra_model_paths.yaml.example
new file mode 100644
index 0000000000000000000000000000000000000000..846d04dbeb43b00bdb08ccb37680eda41beb343e
--- /dev/null
+++ b/ComfyUI/extra_model_paths.yaml.example
@@ -0,0 +1,42 @@
+#Rename this to extra_model_paths.yaml and ComfyUI will load it
+
+
+#config for a1111 ui
+#all you have to do is change the base_path to where yours is installed
+a111:
+ base_path: path/to/stable-diffusion-webui/
+
+ checkpoints: models/Stable-diffusion
+ configs: models/Stable-diffusion
+ vae: models/VAE
+ loras: |
+ models/Lora
+ models/LyCORIS
+ upscale_models: |
+ models/ESRGAN
+ models/RealESRGAN
+ models/SwinIR
+ embeddings: embeddings
+ hypernetworks: models/hypernetworks
+ controlnet: models/ControlNet
+
+#config for comfyui
+#your base path should be either an existing comfy install or a central folder where you store all of your models, loras, etc.
+
+#comfyui:
+# base_path: path/to/comfyui/
+# checkpoints: models/checkpoints/
+# clip: models/clip/
+# clip_vision: models/clip_vision/
+# configs: models/configs/
+# controlnet: models/controlnet/
+# embeddings: models/embeddings/
+# loras: models/loras/
+# upscale_models: models/upscale_models/
+# vae: models/vae/
+
+#other_ui:
+# base_path: path/to/ui
+# checkpoints: models/checkpoints
+# gligen: models/gligen
+# custom_nodes: path/custom_nodes
diff --git a/ComfyUI/folder_paths.py b/ComfyUI/folder_paths.py
new file mode 100644
index 0000000000000000000000000000000000000000..a8726d8dd041728ead949f8ec2d400571fa5070c
--- /dev/null
+++ b/ComfyUI/folder_paths.py
@@ -0,0 +1,247 @@
+import os
+import time
+
+supported_pt_extensions = set(['.ckpt', '.pt', '.bin', '.pth', '.safetensors'])
+
+folder_names_and_paths = {}
+
+base_path = os.path.dirname(os.path.realpath(__file__))
+models_dir = os.path.join(base_path, "models")
+folder_names_and_paths["checkpoints"] = ([os.path.join(models_dir, "checkpoints")], supported_pt_extensions)
+folder_names_and_paths["configs"] = ([os.path.join(models_dir, "configs")], [".yaml"])
+
+folder_names_and_paths["loras"] = ([os.path.join(models_dir, "loras")], supported_pt_extensions)
+folder_names_and_paths["vae"] = ([os.path.join(models_dir, "vae")], supported_pt_extensions)
+folder_names_and_paths["clip"] = ([os.path.join(models_dir, "clip")], supported_pt_extensions)
+folder_names_and_paths["unet"] = ([os.path.join(models_dir, "unet")], supported_pt_extensions)
+folder_names_and_paths["clip_vision"] = ([os.path.join(models_dir, "clip_vision")], supported_pt_extensions)
+folder_names_and_paths["style_models"] = ([os.path.join(models_dir, "style_models")], supported_pt_extensions)
+folder_names_and_paths["embeddings"] = ([os.path.join(models_dir, "embeddings")], supported_pt_extensions)
+folder_names_and_paths["diffusers"] = ([os.path.join(models_dir, "diffusers")], ["folder"])
+folder_names_and_paths["vae_approx"] = ([os.path.join(models_dir, "vae_approx")], supported_pt_extensions)
+
+folder_names_and_paths["controlnet"] = ([os.path.join(models_dir, "controlnet"), os.path.join(models_dir, "t2i_adapter")], supported_pt_extensions)
+folder_names_and_paths["gligen"] = ([os.path.join(models_dir, "gligen")], supported_pt_extensions)
+
+folder_names_and_paths["upscale_models"] = ([os.path.join(models_dir, "upscale_models")], supported_pt_extensions)
+
+folder_names_and_paths["custom_nodes"] = ([os.path.join(base_path, "custom_nodes")], [])
+
+folder_names_and_paths["hypernetworks"] = ([os.path.join(models_dir, "hypernetworks")], supported_pt_extensions)
+
+folder_names_and_paths["classifiers"] = ([os.path.join(models_dir, "classifiers")], {""})
+
+output_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), "output")
+temp_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), "temp")
+input_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), "input")
+
+filename_list_cache = {}
+
+if not os.path.exists(input_directory):
+ try:
+ os.makedirs(input_directory)
+ except:
+ print("Failed to create input directory")
+
+def set_output_directory(output_dir):
+ global output_directory
+ output_directory = output_dir
+
+def set_temp_directory(temp_dir):
+ global temp_directory
+ temp_directory = temp_dir
+
+def set_input_directory(input_dir):
+ global input_directory
+ input_directory = input_dir
+
+def get_output_directory():
+ global output_directory
+ return output_directory
+
+def get_temp_directory():
+ global temp_directory
+ return temp_directory
+
+def get_input_directory():
+ global input_directory
+ return input_directory
+
+
+#NOTE: used in http server so don't put folders that should not be accessed remotely
+def get_directory_by_type(type_name):
+ if type_name == "output":
+ return get_output_directory()
+ if type_name == "temp":
+ return get_temp_directory()
+ if type_name == "input":
+ return get_input_directory()
+ return None
+
+
+# determine base_dir rely on annotation if name is 'filename.ext [annotation]' format
+# otherwise use default_path as base_dir
+def annotated_filepath(name):
+ if name.endswith("[output]"):
+ base_dir = get_output_directory()
+ name = name[:-9]
+ elif name.endswith("[input]"):
+ base_dir = get_input_directory()
+ name = name[:-8]
+ elif name.endswith("[temp]"):
+ base_dir = get_temp_directory()
+ name = name[:-7]
+ else:
+ return name, None
+
+ return name, base_dir
+
+
+def get_annotated_filepath(name, default_dir=None):
+ name, base_dir = annotated_filepath(name)
+
+ if base_dir is None:
+ if default_dir is not None:
+ base_dir = default_dir
+ else:
+ base_dir = get_input_directory() # fallback path
+
+ return os.path.join(base_dir, name)
+
+
+def exists_annotated_filepath(name):
+ name, base_dir = annotated_filepath(name)
+
+ if base_dir is None:
+ base_dir = get_input_directory() # fallback path
+
+ filepath = os.path.join(base_dir, name)
+ return os.path.exists(filepath)
+
+
+def add_model_folder_path(folder_name, full_folder_path):
+ global folder_names_and_paths
+ if folder_name in folder_names_and_paths:
+ folder_names_and_paths[folder_name][0].append(full_folder_path)
+ else:
+ folder_names_and_paths[folder_name] = ([full_folder_path], set())
+
+def get_folder_paths(folder_name):
+ return folder_names_and_paths[folder_name][0][:]
+
+def recursive_search(directory, excluded_dir_names=None):
+ if not os.path.isdir(directory):
+ return [], {}
+
+ if excluded_dir_names is None:
+ excluded_dir_names = []
+
+ result = []
+ dirs = {directory: os.path.getmtime(directory)}
+ for dirpath, subdirs, filenames in os.walk(directory, followlinks=True, topdown=True):
+ subdirs[:] = [d for d in subdirs if d not in excluded_dir_names]
+ for file_name in filenames:
+ relative_path = os.path.relpath(os.path.join(dirpath, file_name), directory)
+ result.append(relative_path)
+ for d in subdirs:
+ path = os.path.join(dirpath, d)
+ dirs[path] = os.path.getmtime(path)
+ return result, dirs
+
+def filter_files_extensions(files, extensions):
+ return sorted(list(filter(lambda a: os.path.splitext(a)[-1].lower() in extensions or len(extensions) == 0, files)))
+
+
+
+def get_full_path(folder_name, filename):
+ global folder_names_and_paths
+ if folder_name not in folder_names_and_paths:
+ return None
+ folders = folder_names_and_paths[folder_name]
+ filename = os.path.relpath(os.path.join("/", filename), "/")
+ for x in folders[0]:
+ full_path = os.path.join(x, filename)
+ if os.path.isfile(full_path):
+ return full_path
+
+ return None
+
+def get_filename_list_(folder_name):
+ global folder_names_and_paths
+ output_list = set()
+ folders = folder_names_and_paths[folder_name]
+ output_folders = {}
+ for x in folders[0]:
+ files, folders_all = recursive_search(x, excluded_dir_names=[".git"])
+ output_list.update(filter_files_extensions(files, folders[1]))
+ output_folders = {**output_folders, **folders_all}
+
+ return (sorted(list(output_list)), output_folders, time.perf_counter())
+
+def cached_filename_list_(folder_name):
+ global filename_list_cache
+ global folder_names_and_paths
+ if folder_name not in filename_list_cache:
+ return None
+ out = filename_list_cache[folder_name]
+
+ for x in out[1]:
+ time_modified = out[1][x]
+ folder = x
+ if os.path.getmtime(folder) != time_modified:
+ return None
+
+ folders = folder_names_and_paths[folder_name]
+ for x in folders[0]:
+ if os.path.isdir(x):
+ if x not in out[1]:
+ return None
+
+ return out
+
+def get_filename_list(folder_name):
+ out = cached_filename_list_(folder_name)
+ if out is None:
+ out = get_filename_list_(folder_name)
+ global filename_list_cache
+ filename_list_cache[folder_name] = out
+ return list(out[0])
+
+def get_save_image_path(filename_prefix, output_dir, image_width=0, image_height=0):
+ def map_filename(filename):
+ prefix_len = len(os.path.basename(filename_prefix))
+ prefix = filename[:prefix_len + 1]
+ try:
+ digits = int(filename[prefix_len + 1:].split('_')[0])
+ except:
+ digits = 0
+ return (digits, prefix)
+
+ def compute_vars(input, image_width, image_height):
+ input = input.replace("%width%", str(image_width))
+ input = input.replace("%height%", str(image_height))
+ return input
+
+ filename_prefix = compute_vars(filename_prefix, image_width, image_height)
+
+ subfolder = os.path.dirname(os.path.normpath(filename_prefix))
+ filename = os.path.basename(os.path.normpath(filename_prefix))
+
+ full_output_folder = os.path.join(output_dir, subfolder)
+
+ if os.path.commonpath((output_dir, os.path.abspath(full_output_folder))) != output_dir:
+ err = "**** ERROR: Saving image outside the output folder is not allowed." + \
+ "\n full_output_folder: " + os.path.abspath(full_output_folder) + \
+ "\n output_dir: " + output_dir + \
+ "\n commonpath: " + os.path.commonpath((output_dir, os.path.abspath(full_output_folder)))
+ print(err)
+ raise Exception(err)
+
+ try:
+ counter = max(filter(lambda a: a[1][:-1] == filename and a[1][-1] == "_", map(map_filename, os.listdir(full_output_folder))))[0] + 1
+ except ValueError:
+ counter = 1
+ except FileNotFoundError:
+ os.makedirs(full_output_folder, exist_ok=True)
+ counter = 1
+ return full_output_folder, filename, counter, subfolder, filename_prefix
diff --git a/ComfyUI/input/Screenshot 2023-12-30 182919.jpg b/ComfyUI/input/Screenshot 2023-12-30 182919.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..978ea3ed3fff6e18cfce70bbd853f0db339399e6
Binary files /dev/null and b/ComfyUI/input/Screenshot 2023-12-30 182919.jpg differ
diff --git a/ComfyUI/input/example.png b/ComfyUI/input/example.png
new file mode 100644
index 0000000000000000000000000000000000000000..7b7f3c9cbbe6d8750c4a9eaf65d6ae4d2f108f79
Binary files /dev/null and b/ComfyUI/input/example.png differ
diff --git a/ComfyUI/input/gsro3x7zylwa1_resized.png b/ComfyUI/input/gsro3x7zylwa1_resized.png
new file mode 100644
index 0000000000000000000000000000000000000000..c3e6713ec3e20eca4c8a586363be32ddfe050ac0
--- /dev/null
+++ b/ComfyUI/input/gsro3x7zylwa1_resized.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:78b24a0276389ea7da2217757579716fca8fc7ba3f07afe8840b22cbce2f2c33
+size 1099009
diff --git a/ComfyUI/latent_preview.py b/ComfyUI/latent_preview.py
new file mode 100644
index 0000000000000000000000000000000000000000..61754751efee96fb25c18b19ff2032a875ca221a
--- /dev/null
+++ b/ComfyUI/latent_preview.py
@@ -0,0 +1,97 @@
+import torch
+from PIL import Image
+import struct
+import numpy as np
+from comfy.cli_args import args, LatentPreviewMethod
+from comfy.taesd.taesd import TAESD
+import folder_paths
+import comfy.utils
+
+MAX_PREVIEW_RESOLUTION = 512
+
+class LatentPreviewer:
+ def decode_latent_to_preview(self, x0):
+ pass
+
+ def decode_latent_to_preview_image(self, preview_format, x0):
+ preview_image = self.decode_latent_to_preview(x0)
+ return ("JPEG", preview_image, MAX_PREVIEW_RESOLUTION)
+
+class TAESDPreviewerImpl(LatentPreviewer):
+ def __init__(self, taesd):
+ self.taesd = taesd
+
+ def decode_latent_to_preview(self, x0):
+ x_sample = self.taesd.decode(x0[:1])[0].detach()
+ x_sample = torch.clamp((x_sample + 1.0) / 2.0, min=0.0, max=1.0)
+ x_sample = 255. * np.moveaxis(x_sample.cpu().numpy(), 0, 2)
+ x_sample = x_sample.astype(np.uint8)
+
+ preview_image = Image.fromarray(x_sample)
+ return preview_image
+
+
+class Latent2RGBPreviewer(LatentPreviewer):
+ def __init__(self, latent_rgb_factors):
+ self.latent_rgb_factors = torch.tensor(latent_rgb_factors, device="cpu")
+
+ def decode_latent_to_preview(self, x0):
+ latent_image = x0[0].permute(1, 2, 0).cpu() @ self.latent_rgb_factors
+
+ latents_ubyte = (((latent_image + 1) / 2)
+ .clamp(0, 1) # change scale from -1..1 to 0..1
+ .mul(0xFF) # to 0..255
+ .byte()).cpu()
+
+ return Image.fromarray(latents_ubyte.numpy())
+
+
+def get_previewer(device, latent_format):
+ previewer = None
+ method = args.preview_method
+ if method != LatentPreviewMethod.NoPreviews:
+ # TODO previewer methods
+ taesd_decoder_path = None
+ if latent_format.taesd_decoder_name is not None:
+ taesd_decoder_path = next(
+ (fn for fn in folder_paths.get_filename_list("vae_approx")
+ if fn.startswith(latent_format.taesd_decoder_name)),
+ ""
+ )
+ taesd_decoder_path = folder_paths.get_full_path("vae_approx", taesd_decoder_path)
+
+ if method == LatentPreviewMethod.Auto:
+ method = LatentPreviewMethod.Latent2RGB
+ if taesd_decoder_path:
+ method = LatentPreviewMethod.TAESD
+
+ if method == LatentPreviewMethod.TAESD:
+ if taesd_decoder_path:
+ taesd = TAESD(None, taesd_decoder_path).to(device)
+ previewer = TAESDPreviewerImpl(taesd)
+ else:
+ print("Warning: TAESD previews enabled, but could not find models/vae_approx/{}".format(latent_format.taesd_decoder_name))
+
+ if previewer is None:
+ if latent_format.latent_rgb_factors is not None:
+ previewer = Latent2RGBPreviewer(latent_format.latent_rgb_factors)
+ return previewer
+
+def prepare_callback(model, steps, x0_output_dict=None):
+ preview_format = "JPEG"
+ if preview_format not in ["JPEG", "PNG"]:
+ preview_format = "JPEG"
+
+ previewer = get_previewer(model.load_device, model.model.latent_format)
+
+ pbar = comfy.utils.ProgressBar(steps)
+ def callback(step, x0, x, total_steps):
+ if x0_output_dict is not None:
+ x0_output_dict["x0"] = x0
+
+ preview_bytes = None
+ if previewer:
+ preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0)
+ pbar.update_absolute(step + 1, total_steps, preview_bytes)
+ return callback
+
diff --git a/ComfyUI/main.py b/ComfyUI/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..f6aeceed2afb1890ca4af6f5ffda8fc4e63d57d1
--- /dev/null
+++ b/ComfyUI/main.py
@@ -0,0 +1,228 @@
+import comfy.options
+comfy.options.enable_args_parsing()
+
+import os
+import importlib.util
+import folder_paths
+import time
+
+def execute_prestartup_script():
+ def execute_script(script_path):
+ module_name = os.path.splitext(script_path)[0]
+ try:
+ spec = importlib.util.spec_from_file_location(module_name, script_path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return True
+ except Exception as e:
+ print(f"Failed to execute startup-script: {script_path} / {e}")
+ return False
+
+ node_paths = folder_paths.get_folder_paths("custom_nodes")
+ for custom_node_path in node_paths:
+ possible_modules = os.listdir(custom_node_path)
+ node_prestartup_times = []
+
+ for possible_module in possible_modules:
+ module_path = os.path.join(custom_node_path, possible_module)
+ if os.path.isfile(module_path) or module_path.endswith(".disabled") or module_path == "__pycache__":
+ continue
+
+ script_path = os.path.join(module_path, "prestartup_script.py")
+ if os.path.exists(script_path):
+ time_before = time.perf_counter()
+ success = execute_script(script_path)
+ node_prestartup_times.append((time.perf_counter() - time_before, module_path, success))
+ if len(node_prestartup_times) > 0:
+ print("\nPrestartup times for custom nodes:")
+ for n in sorted(node_prestartup_times):
+ if n[2]:
+ import_message = ""
+ else:
+ import_message = " (PRESTARTUP FAILED)"
+ print("{:6.1f} seconds{}:".format(n[0], import_message), n[1])
+ print()
+
+execute_prestartup_script()
+
+
+# Main code
+import asyncio
+import itertools
+import shutil
+import threading
+import gc
+
+from comfy.cli_args import args
+
+if os.name == "nt":
+ import logging
+ logging.getLogger("xformers").addFilter(lambda record: 'A matching Triton is not available' not in record.getMessage())
+
+if __name__ == "__main__":
+ if args.cuda_device is not None:
+ os.environ['CUDA_VISIBLE_DEVICES'] = str(args.cuda_device)
+ print("Set cuda device to:", args.cuda_device)
+
+ if args.deterministic:
+ if 'CUBLAS_WORKSPACE_CONFIG' not in os.environ:
+ os.environ['CUBLAS_WORKSPACE_CONFIG'] = ":4096:8"
+
+ import cuda_malloc
+
+import comfy.utils
+import yaml
+
+import execution
+import server
+from server import BinaryEventTypes
+from nodes import init_custom_nodes
+import comfy.model_management
+
+def cuda_malloc_warning():
+ device = comfy.model_management.get_torch_device()
+ device_name = comfy.model_management.get_torch_device_name(device)
+ cuda_malloc_warning = False
+ if "cudaMallocAsync" in device_name:
+ for b in cuda_malloc.blacklist:
+ if b in device_name:
+ cuda_malloc_warning = True
+ if cuda_malloc_warning:
+ print("\nWARNING: this card most likely does not support cuda-malloc, if you get \"CUDA error\" please run ComfyUI with: --disable-cuda-malloc\n")
+
+def prompt_worker(q, server):
+ e = execution.PromptExecutor(server)
+ last_gc_collect = 0
+ need_gc = False
+ gc_collect_interval = 10.0
+
+ while True:
+ timeout = None
+ if need_gc:
+ timeout = max(gc_collect_interval - (current_time - last_gc_collect), 0.0)
+
+ queue_item = q.get(timeout=timeout)
+ if queue_item is not None:
+ item, item_id = queue_item
+ execution_start_time = time.perf_counter()
+ prompt_id = item[1]
+ e.execute(item[2], prompt_id, item[3], item[4])
+ need_gc = True
+ q.task_done(item_id, e.outputs_ui)
+ if server.client_id is not None:
+ server.send_sync("executing", { "node": None, "prompt_id": prompt_id }, server.client_id)
+
+ current_time = time.perf_counter()
+ execution_time = current_time - execution_start_time
+ print("Prompt executed in {:.2f} seconds".format(execution_time))
+
+ if need_gc:
+ current_time = time.perf_counter()
+ if (current_time - last_gc_collect) > gc_collect_interval:
+ gc.collect()
+ comfy.model_management.soft_empty_cache()
+ last_gc_collect = current_time
+ need_gc = False
+
+async def run(server, address='', port=8188, verbose=True, call_on_start=None):
+ await asyncio.gather(server.start(address, port, verbose, call_on_start), server.publish_loop())
+
+
+def hijack_progress(server):
+ def hook(value, total, preview_image):
+ comfy.model_management.throw_exception_if_processing_interrupted()
+ server.send_sync("progress", {"value": value, "max": total}, server.client_id)
+ if preview_image is not None:
+ server.send_sync(BinaryEventTypes.UNENCODED_PREVIEW_IMAGE, preview_image, server.client_id)
+ comfy.utils.set_progress_bar_global_hook(hook)
+
+
+def cleanup_temp():
+ temp_dir = folder_paths.get_temp_directory()
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir, ignore_errors=True)
+
+
+def load_extra_path_config(yaml_path):
+ with open(yaml_path, 'r') as stream:
+ config = yaml.safe_load(stream)
+ for c in config:
+ conf = config[c]
+ if conf is None:
+ continue
+ base_path = None
+ if "base_path" in conf:
+ base_path = conf.pop("base_path")
+ for x in conf:
+ for y in conf[x].split("\n"):
+ if len(y) == 0:
+ continue
+ full_path = y
+ if base_path is not None:
+ full_path = os.path.join(base_path, full_path)
+ print("Adding extra search path", x, full_path)
+ folder_paths.add_model_folder_path(x, full_path)
+
+
+if __name__ == "__main__":
+ if args.temp_directory:
+ temp_dir = os.path.join(os.path.abspath(args.temp_directory), "temp")
+ print(f"Setting temp directory to: {temp_dir}")
+ folder_paths.set_temp_directory(temp_dir)
+ cleanup_temp()
+
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ server = server.PromptServer(loop)
+ q = execution.PromptQueue(server)
+
+ extra_model_paths_config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "extra_model_paths.yaml")
+ if os.path.isfile(extra_model_paths_config_path):
+ load_extra_path_config(extra_model_paths_config_path)
+
+ if args.extra_model_paths_config:
+ for config_path in itertools.chain(*args.extra_model_paths_config):
+ load_extra_path_config(config_path)
+
+ init_custom_nodes()
+
+ cuda_malloc_warning()
+
+ server.add_routes()
+ hijack_progress(server)
+
+ threading.Thread(target=prompt_worker, daemon=True, args=(q, server,)).start()
+
+ if args.output_directory:
+ output_dir = os.path.abspath(args.output_directory)
+ print(f"Setting output directory to: {output_dir}")
+ folder_paths.set_output_directory(output_dir)
+
+ #These are the default folders that checkpoints, clip and vae models will be saved to when using CheckpointSave, etc.. nodes
+ folder_paths.add_model_folder_path("checkpoints", os.path.join(folder_paths.get_output_directory(), "checkpoints"))
+ folder_paths.add_model_folder_path("clip", os.path.join(folder_paths.get_output_directory(), "clip"))
+ folder_paths.add_model_folder_path("vae", os.path.join(folder_paths.get_output_directory(), "vae"))
+
+ if args.input_directory:
+ input_dir = os.path.abspath(args.input_directory)
+ print(f"Setting input directory to: {input_dir}")
+ folder_paths.set_input_directory(input_dir)
+
+ if args.quick_test_for_ci:
+ exit(0)
+
+ call_on_start = None
+ if args.auto_launch:
+ def startup_server(address, port):
+ import webbrowser
+ if os.name == 'nt' and address == '0.0.0.0':
+ address = '127.0.0.1'
+ webbrowser.open(f"http://{address}:{port}")
+ call_on_start = startup_server
+
+ try:
+ loop.run_until_complete(run(server, address=args.listen, port=args.port, verbose=not args.dont_print_server, call_on_start=call_on_start))
+ except KeyboardInterrupt:
+ print("\nStopped server")
+
+ cleanup_temp()
diff --git a/ComfyUI/models/checkpoints/SDv2-768.ckpt b/ComfyUI/models/checkpoints/SDv2-768.ckpt
new file mode 100644
index 0000000000000000000000000000000000000000..b07ed31d364d0f4d9e223ce22115f0f61a682109
--- /dev/null
+++ b/ComfyUI/models/checkpoints/SDv2-768.ckpt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:43c90409bd4493ea9f4734a166fc6180913f97cef4f69a2b7883edf700779693
+size 2607544561
diff --git a/ComfyUI/models/checkpoints/darkSushiMixMix_225D.safetensors b/ComfyUI/models/checkpoints/darkSushiMixMix_225D.safetensors
new file mode 100644
index 0000000000000000000000000000000000000000..acec6293eca607d0f39da20ef363a7a3b84542b2
--- /dev/null
+++ b/ComfyUI/models/checkpoints/darkSushiMixMix_225D.safetensors
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cca17b08da3254aa2f0242a84ab8641c89d1bc71cc1046fc79f8e906581a97ba
+size 2132627182
diff --git a/ComfyUI/models/checkpoints/put_checkpoints_here b/ComfyUI/models/checkpoints/put_checkpoints_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/checkpoints/sd_xl_base_1.0.safetensors b/ComfyUI/models/checkpoints/sd_xl_base_1.0.safetensors
new file mode 100644
index 0000000000000000000000000000000000000000..a4e26e69370f72c43e5b5f53879919c86bcd6822
--- /dev/null
+++ b/ComfyUI/models/checkpoints/sd_xl_base_1.0.safetensors
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:31e35c80fc4829d14f90153f4c74cd59c90b779f6afe05a74cd6120b893f7e5b
+size 6938078334
diff --git a/ComfyUI/models/checkpoints/sd_xl_refiner_1.0.safetensors b/ComfyUI/models/checkpoints/sd_xl_refiner_1.0.safetensors
new file mode 100644
index 0000000000000000000000000000000000000000..73f461bed1c6ccf74548ebd9da0e62122f7a25bd
--- /dev/null
+++ b/ComfyUI/models/checkpoints/sd_xl_refiner_1.0.safetensors
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7440042bbdc8a24813002c09b6b69b64dc90fded4472613437b7f55f9b7d9c5f
+size 6075981930
diff --git a/ComfyUI/models/checkpoints/svd_xt.safetensors b/ComfyUI/models/checkpoints/svd_xt.safetensors
new file mode 100644
index 0000000000000000000000000000000000000000..834e6b8c6f74a1a5e530eda031edd5c43993b4e2
--- /dev/null
+++ b/ComfyUI/models/checkpoints/svd_xt.safetensors
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b2652c23d64a1da5f14d55011b9b6dce55f2e72e395719f1cd1f8a079b00a451
+size 9559625980
diff --git a/ComfyUI/models/checkpoints/v1-5-pruned-emaonly.ckpt b/ComfyUI/models/checkpoints/v1-5-pruned-emaonly.ckpt
new file mode 100644
index 0000000000000000000000000000000000000000..bdc7ea15824c46103a08ba376b47eebf09f36895
--- /dev/null
+++ b/ComfyUI/models/checkpoints/v1-5-pruned-emaonly.ckpt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4c86efd062ea772972dd068c3870773f1fd9a37ba9a288ee79ec848448ce7785
+size 2132791380
diff --git a/ComfyUI/models/clip/put_clip_or_text_encoder_models_here b/ComfyUI/models/clip/put_clip_or_text_encoder_models_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/clip_vision/put_clip_vision_models_here b/ComfyUI/models/clip_vision/put_clip_vision_models_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/configs/anything_v3.yaml b/ComfyUI/models/configs/anything_v3.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8bcfe584ae73d60e2c7a6f89b3f7befbd487ea34
--- /dev/null
+++ b/ComfyUI/models/configs/anything_v3.yaml
@@ -0,0 +1,73 @@
+model:
+ base_learning_rate: 1.0e-04
+ target: ldm.models.diffusion.ddpm.LatentDiffusion
+ params:
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false # Note: different from the one we trained before
+ conditioning_key: crossattn
+ monitor: val/loss_simple_ema
+ scale_factor: 0.18215
+ use_ema: False
+
+ scheduler_config: # 10000 warmup steps
+ target: ldm.lr_scheduler.LambdaLinearScheduler
+ params:
+ warm_up_steps: [ 10000 ]
+ cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases
+ f_start: [ 1.e-6 ]
+ f_max: [ 1. ]
+ f_min: [ 1. ]
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ image_size: 32 # unused
+ in_channels: 4
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_heads: 8
+ use_spatial_transformer: True
+ transformer_depth: 1
+ context_dim: 768
+ use_checkpoint: True
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: []
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenCLIPEmbedder
+ params:
+ layer: "hidden"
+ layer_idx: -2
diff --git a/ComfyUI/models/configs/v1-inference.yaml b/ComfyUI/models/configs/v1-inference.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d4effe569e897369918625f9d8be5603a0e6a0d6
--- /dev/null
+++ b/ComfyUI/models/configs/v1-inference.yaml
@@ -0,0 +1,70 @@
+model:
+ base_learning_rate: 1.0e-04
+ target: ldm.models.diffusion.ddpm.LatentDiffusion
+ params:
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false # Note: different from the one we trained before
+ conditioning_key: crossattn
+ monitor: val/loss_simple_ema
+ scale_factor: 0.18215
+ use_ema: False
+
+ scheduler_config: # 10000 warmup steps
+ target: ldm.lr_scheduler.LambdaLinearScheduler
+ params:
+ warm_up_steps: [ 10000 ]
+ cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases
+ f_start: [ 1.e-6 ]
+ f_max: [ 1. ]
+ f_min: [ 1. ]
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ image_size: 32 # unused
+ in_channels: 4
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_heads: 8
+ use_spatial_transformer: True
+ transformer_depth: 1
+ context_dim: 768
+ use_checkpoint: True
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: []
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenCLIPEmbedder
diff --git a/ComfyUI/models/configs/v1-inference_clip_skip_2.yaml b/ComfyUI/models/configs/v1-inference_clip_skip_2.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8bcfe584ae73d60e2c7a6f89b3f7befbd487ea34
--- /dev/null
+++ b/ComfyUI/models/configs/v1-inference_clip_skip_2.yaml
@@ -0,0 +1,73 @@
+model:
+ base_learning_rate: 1.0e-04
+ target: ldm.models.diffusion.ddpm.LatentDiffusion
+ params:
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false # Note: different from the one we trained before
+ conditioning_key: crossattn
+ monitor: val/loss_simple_ema
+ scale_factor: 0.18215
+ use_ema: False
+
+ scheduler_config: # 10000 warmup steps
+ target: ldm.lr_scheduler.LambdaLinearScheduler
+ params:
+ warm_up_steps: [ 10000 ]
+ cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases
+ f_start: [ 1.e-6 ]
+ f_max: [ 1. ]
+ f_min: [ 1. ]
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ image_size: 32 # unused
+ in_channels: 4
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_heads: 8
+ use_spatial_transformer: True
+ transformer_depth: 1
+ context_dim: 768
+ use_checkpoint: True
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: []
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenCLIPEmbedder
+ params:
+ layer: "hidden"
+ layer_idx: -2
diff --git a/ComfyUI/models/configs/v1-inference_clip_skip_2_fp16.yaml b/ComfyUI/models/configs/v1-inference_clip_skip_2_fp16.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7eca31c7b5e571c2b1348e94ed9d69978ebd2d52
--- /dev/null
+++ b/ComfyUI/models/configs/v1-inference_clip_skip_2_fp16.yaml
@@ -0,0 +1,74 @@
+model:
+ base_learning_rate: 1.0e-04
+ target: ldm.models.diffusion.ddpm.LatentDiffusion
+ params:
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false # Note: different from the one we trained before
+ conditioning_key: crossattn
+ monitor: val/loss_simple_ema
+ scale_factor: 0.18215
+ use_ema: False
+
+ scheduler_config: # 10000 warmup steps
+ target: ldm.lr_scheduler.LambdaLinearScheduler
+ params:
+ warm_up_steps: [ 10000 ]
+ cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases
+ f_start: [ 1.e-6 ]
+ f_max: [ 1. ]
+ f_min: [ 1. ]
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ use_fp16: True
+ image_size: 32 # unused
+ in_channels: 4
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_heads: 8
+ use_spatial_transformer: True
+ transformer_depth: 1
+ context_dim: 768
+ use_checkpoint: True
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: []
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenCLIPEmbedder
+ params:
+ layer: "hidden"
+ layer_idx: -2
diff --git a/ComfyUI/models/configs/v1-inference_fp16.yaml b/ComfyUI/models/configs/v1-inference_fp16.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..147f42b17b835cc839338156f99e8f971df5c1aa
--- /dev/null
+++ b/ComfyUI/models/configs/v1-inference_fp16.yaml
@@ -0,0 +1,71 @@
+model:
+ base_learning_rate: 1.0e-04
+ target: ldm.models.diffusion.ddpm.LatentDiffusion
+ params:
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false # Note: different from the one we trained before
+ conditioning_key: crossattn
+ monitor: val/loss_simple_ema
+ scale_factor: 0.18215
+ use_ema: False
+
+ scheduler_config: # 10000 warmup steps
+ target: ldm.lr_scheduler.LambdaLinearScheduler
+ params:
+ warm_up_steps: [ 10000 ]
+ cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases
+ f_start: [ 1.e-6 ]
+ f_max: [ 1. ]
+ f_min: [ 1. ]
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ use_fp16: True
+ image_size: 32 # unused
+ in_channels: 4
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_heads: 8
+ use_spatial_transformer: True
+ transformer_depth: 1
+ context_dim: 768
+ use_checkpoint: True
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: []
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenCLIPEmbedder
diff --git a/ComfyUI/models/configs/v1-inpainting-inference.yaml b/ComfyUI/models/configs/v1-inpainting-inference.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..45f3f82d461cd8c6109f26ec3b1da75366eda0b0
--- /dev/null
+++ b/ComfyUI/models/configs/v1-inpainting-inference.yaml
@@ -0,0 +1,71 @@
+model:
+ base_learning_rate: 7.5e-05
+ target: ldm.models.diffusion.ddpm.LatentInpaintDiffusion
+ params:
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false # Note: different from the one we trained before
+ conditioning_key: hybrid # important
+ monitor: val/loss_simple_ema
+ scale_factor: 0.18215
+ finetune_keys: null
+
+ scheduler_config: # 10000 warmup steps
+ target: ldm.lr_scheduler.LambdaLinearScheduler
+ params:
+ warm_up_steps: [ 2500 ] # NOTE for resuming. use 10000 if starting from scratch
+ cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases
+ f_start: [ 1.e-6 ]
+ f_max: [ 1. ]
+ f_min: [ 1. ]
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ image_size: 32 # unused
+ in_channels: 9 # 4 data + 4 downscaled image + 1 mask
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_heads: 8
+ use_spatial_transformer: True
+ transformer_depth: 1
+ context_dim: 768
+ use_checkpoint: True
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: []
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenCLIPEmbedder
+
diff --git a/ComfyUI/models/configs/v2-inference-v.yaml b/ComfyUI/models/configs/v2-inference-v.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8ec8dfbfefe94ae8522c93017668fea78d580acf
--- /dev/null
+++ b/ComfyUI/models/configs/v2-inference-v.yaml
@@ -0,0 +1,68 @@
+model:
+ base_learning_rate: 1.0e-4
+ target: ldm.models.diffusion.ddpm.LatentDiffusion
+ params:
+ parameterization: "v"
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false
+ conditioning_key: crossattn
+ monitor: val/loss_simple_ema
+ scale_factor: 0.18215
+ use_ema: False # we set this to false because this is an inference only config
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ use_checkpoint: True
+ use_fp16: True
+ image_size: 32 # unused
+ in_channels: 4
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_head_channels: 64 # need to fix for flash-attn
+ use_spatial_transformer: True
+ use_linear_in_transformer: True
+ transformer_depth: 1
+ context_dim: 1024
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ #attn_type: "vanilla-xformers"
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: []
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder
+ params:
+ freeze: True
+ layer: "penultimate"
diff --git a/ComfyUI/models/configs/v2-inference-v_fp32.yaml b/ComfyUI/models/configs/v2-inference-v_fp32.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d5c9b9cb29ca162ade44a7c922f59e75d7d57813
--- /dev/null
+++ b/ComfyUI/models/configs/v2-inference-v_fp32.yaml
@@ -0,0 +1,68 @@
+model:
+ base_learning_rate: 1.0e-4
+ target: ldm.models.diffusion.ddpm.LatentDiffusion
+ params:
+ parameterization: "v"
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false
+ conditioning_key: crossattn
+ monitor: val/loss_simple_ema
+ scale_factor: 0.18215
+ use_ema: False # we set this to false because this is an inference only config
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ use_checkpoint: True
+ use_fp16: False
+ image_size: 32 # unused
+ in_channels: 4
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_head_channels: 64 # need to fix for flash-attn
+ use_spatial_transformer: True
+ use_linear_in_transformer: True
+ transformer_depth: 1
+ context_dim: 1024
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ #attn_type: "vanilla-xformers"
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: []
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder
+ params:
+ freeze: True
+ layer: "penultimate"
diff --git a/ComfyUI/models/configs/v2-inference.yaml b/ComfyUI/models/configs/v2-inference.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..152c4f3c2b36c3b246a9cb10eb8166134b0d2e1c
--- /dev/null
+++ b/ComfyUI/models/configs/v2-inference.yaml
@@ -0,0 +1,67 @@
+model:
+ base_learning_rate: 1.0e-4
+ target: ldm.models.diffusion.ddpm.LatentDiffusion
+ params:
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false
+ conditioning_key: crossattn
+ monitor: val/loss_simple_ema
+ scale_factor: 0.18215
+ use_ema: False # we set this to false because this is an inference only config
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ use_checkpoint: True
+ use_fp16: True
+ image_size: 32 # unused
+ in_channels: 4
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_head_channels: 64 # need to fix for flash-attn
+ use_spatial_transformer: True
+ use_linear_in_transformer: True
+ transformer_depth: 1
+ context_dim: 1024
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ #attn_type: "vanilla-xformers"
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: []
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder
+ params:
+ freeze: True
+ layer: "penultimate"
diff --git a/ComfyUI/models/configs/v2-inference_fp32.yaml b/ComfyUI/models/configs/v2-inference_fp32.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0d03231f3f2c2e8ef8fbe0d781e5f3d65409ef3a
--- /dev/null
+++ b/ComfyUI/models/configs/v2-inference_fp32.yaml
@@ -0,0 +1,67 @@
+model:
+ base_learning_rate: 1.0e-4
+ target: ldm.models.diffusion.ddpm.LatentDiffusion
+ params:
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false
+ conditioning_key: crossattn
+ monitor: val/loss_simple_ema
+ scale_factor: 0.18215
+ use_ema: False # we set this to false because this is an inference only config
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ use_checkpoint: True
+ use_fp16: False
+ image_size: 32 # unused
+ in_channels: 4
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_head_channels: 64 # need to fix for flash-attn
+ use_spatial_transformer: True
+ use_linear_in_transformer: True
+ transformer_depth: 1
+ context_dim: 1024
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ #attn_type: "vanilla-xformers"
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: []
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder
+ params:
+ freeze: True
+ layer: "penultimate"
diff --git a/ComfyUI/models/configs/v2-inpainting-inference.yaml b/ComfyUI/models/configs/v2-inpainting-inference.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..32a9471d71b828c51bcbbabfe34c5f6c8282c803
--- /dev/null
+++ b/ComfyUI/models/configs/v2-inpainting-inference.yaml
@@ -0,0 +1,158 @@
+model:
+ base_learning_rate: 5.0e-05
+ target: ldm.models.diffusion.ddpm.LatentInpaintDiffusion
+ params:
+ linear_start: 0.00085
+ linear_end: 0.0120
+ num_timesteps_cond: 1
+ log_every_t: 200
+ timesteps: 1000
+ first_stage_key: "jpg"
+ cond_stage_key: "txt"
+ image_size: 64
+ channels: 4
+ cond_stage_trainable: false
+ conditioning_key: hybrid
+ scale_factor: 0.18215
+ monitor: val/loss_simple_ema
+ finetune_keys: null
+ use_ema: False
+
+ unet_config:
+ target: ldm.modules.diffusionmodules.openaimodel.UNetModel
+ params:
+ use_checkpoint: True
+ image_size: 32 # unused
+ in_channels: 9
+ out_channels: 4
+ model_channels: 320
+ attention_resolutions: [ 4, 2, 1 ]
+ num_res_blocks: 2
+ channel_mult: [ 1, 2, 4, 4 ]
+ num_head_channels: 64 # need to fix for flash-attn
+ use_spatial_transformer: True
+ use_linear_in_transformer: True
+ transformer_depth: 1
+ context_dim: 1024
+ legacy: False
+
+ first_stage_config:
+ target: ldm.models.autoencoder.AutoencoderKL
+ params:
+ embed_dim: 4
+ monitor: val/rec_loss
+ ddconfig:
+ #attn_type: "vanilla-xformers"
+ double_z: true
+ z_channels: 4
+ resolution: 256
+ in_channels: 3
+ out_ch: 3
+ ch: 128
+ ch_mult:
+ - 1
+ - 2
+ - 4
+ - 4
+ num_res_blocks: 2
+ attn_resolutions: [ ]
+ dropout: 0.0
+ lossconfig:
+ target: torch.nn.Identity
+
+ cond_stage_config:
+ target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder
+ params:
+ freeze: True
+ layer: "penultimate"
+
+
+data:
+ target: ldm.data.laion.WebDataModuleFromConfig
+ params:
+ tar_base: null # for concat as in LAION-A
+ p_unsafe_threshold: 0.1
+ filter_word_list: "data/filters.yaml"
+ max_pwatermark: 0.45
+ batch_size: 8
+ num_workers: 6
+ multinode: True
+ min_size: 512
+ train:
+ shards:
+ - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-0/{00000..18699}.tar -"
+ - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-1/{00000..18699}.tar -"
+ - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-2/{00000..18699}.tar -"
+ - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-3/{00000..18699}.tar -"
+ - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-4/{00000..18699}.tar -" #{00000-94333}.tar"
+ shuffle: 10000
+ image_key: jpg
+ image_transforms:
+ - target: torchvision.transforms.Resize
+ params:
+ size: 512
+ interpolation: 3
+ - target: torchvision.transforms.RandomCrop
+ params:
+ size: 512
+ postprocess:
+ target: ldm.data.laion.AddMask
+ params:
+ mode: "512train-large"
+ p_drop: 0.25
+ # NOTE use enough shards to avoid empty validation loops in workers
+ validation:
+ shards:
+ - "pipe:aws s3 cp s3://deep-floyd-s3/datasets/laion_cleaned-part5/{93001..94333}.tar - "
+ shuffle: 0
+ image_key: jpg
+ image_transforms:
+ - target: torchvision.transforms.Resize
+ params:
+ size: 512
+ interpolation: 3
+ - target: torchvision.transforms.CenterCrop
+ params:
+ size: 512
+ postprocess:
+ target: ldm.data.laion.AddMask
+ params:
+ mode: "512train-large"
+ p_drop: 0.25
+
+lightning:
+ find_unused_parameters: True
+ modelcheckpoint:
+ params:
+ every_n_train_steps: 5000
+
+ callbacks:
+ metrics_over_trainsteps_checkpoint:
+ params:
+ every_n_train_steps: 10000
+
+ image_logger:
+ target: main.ImageLogger
+ params:
+ enable_autocast: False
+ disabled: False
+ batch_frequency: 1000
+ max_images: 4
+ increase_log_steps: False
+ log_first_step: False
+ log_images_kwargs:
+ use_ema_scope: False
+ inpaint: False
+ plot_progressive_rows: False
+ plot_diffusion_rows: False
+ N: 4
+ unconditional_guidance_scale: 5.0
+ unconditional_guidance_label: [""]
+ ddim_steps: 50 # todo check these out for depth2img,
+ ddim_eta: 0.0 # todo check these out for depth2img,
+
+ trainer:
+ benchmark: True
+ val_check_interval: 5000000
+ num_sanity_val_steps: 0
+ accumulate_grad_batches: 1
diff --git a/ComfyUI/models/controlnet/put_controlnets_and_t2i_here b/ComfyUI/models/controlnet/put_controlnets_and_t2i_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/diffusers/put_diffusers_models_here b/ComfyUI/models/diffusers/put_diffusers_models_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/embeddings/bad-artist-anime.pt b/ComfyUI/models/embeddings/bad-artist-anime.pt
new file mode 100644
index 0000000000000000000000000000000000000000..41dc1ef4174928ea838448c87d06f5926dfa4ef2
--- /dev/null
+++ b/ComfyUI/models/embeddings/bad-artist-anime.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5f7bea88750c97a0b8c9ba9f5bc0d13648c3a17a69aaac855903229d5f58c34b
+size 7083
diff --git a/ComfyUI/models/embeddings/bad-hands-5.pt b/ComfyUI/models/embeddings/bad-hands-5.pt
new file mode 100644
index 0000000000000000000000000000000000000000..b6bc361f5f8b9bd6b840f64d5fbdcfec33b770cd
--- /dev/null
+++ b/ComfyUI/models/embeddings/bad-hands-5.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aa7651be154c46a2f4868788ef84a92b3083b0c0c5c46f5012a56698bfd2a1ba
+size 7083
diff --git a/ComfyUI/models/embeddings/bad-picture-chill-75v.pt b/ComfyUI/models/embeddings/bad-picture-chill-75v.pt
new file mode 100644
index 0000000000000000000000000000000000000000..a7e141e998366ceacd1020164612a891f529dd25
--- /dev/null
+++ b/ComfyUI/models/embeddings/bad-picture-chill-75v.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7d9cc5f549d7972f24803a3a9880a923fb9a1b68c1443da3ce1ff2f7eff25ae9
+size 231524
diff --git a/ComfyUI/models/embeddings/ng_deepnegative_v1_75t.pt b/ComfyUI/models/embeddings/ng_deepnegative_v1_75t.pt
new file mode 100644
index 0000000000000000000000000000000000000000..849edda8ba90c8cf83865abd84ed828f23720592
--- /dev/null
+++ b/ComfyUI/models/embeddings/ng_deepnegative_v1_75t.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:54e7e4826d53949a3d0dde40aea023b1e456a618c608a7630e3999fd38f93245
+size 231339
diff --git a/ComfyUI/models/embeddings/put_embeddings_or_textual_inversion_concepts_here b/ComfyUI/models/embeddings/put_embeddings_or_textual_inversion_concepts_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/gligen/put_gligen_models_here b/ComfyUI/models/gligen/put_gligen_models_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/hypernetworks/put_hypernetworks_here b/ComfyUI/models/hypernetworks/put_hypernetworks_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/loras/edgBodytape_MINI.safetensors b/ComfyUI/models/loras/edgBodytape_MINI.safetensors
new file mode 100644
index 0000000000000000000000000000000000000000..1e7ffd648ddc4052ec4f8f1e403caca2779d3dcd
--- /dev/null
+++ b/ComfyUI/models/loras/edgBodytape_MINI.safetensors
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:942c76ad90d3f53ffe3f2f8dac21cf5a52ee31687766b8657788a7963075e1ac
+size 9551832
diff --git a/ComfyUI/models/loras/mlegs1-000015.safetensors b/ComfyUI/models/loras/mlegs1-000015.safetensors
new file mode 100644
index 0000000000000000000000000000000000000000..ec139d82a16437b48c6e02293169e2d13f922fbd
--- /dev/null
+++ b/ComfyUI/models/loras/mlegs1-000015.safetensors
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cd39c2181438a7f7a22ef108c5c81ef7ff8ff999bb5444b265db1f06756fa28c
+size 9642711
diff --git a/ComfyUI/models/loras/put_loras_here b/ComfyUI/models/loras/put_loras_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/sams/sam_vit_b_01ec64.pth b/ComfyUI/models/sams/sam_vit_b_01ec64.pth
new file mode 100644
index 0000000000000000000000000000000000000000..ab7d111e57bd052a76fe669986560e3555e9c8f6
--- /dev/null
+++ b/ComfyUI/models/sams/sam_vit_b_01ec64.pth
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ec2df62732614e57411cdcf32a23ffdf28910380d03139ee0f4fcbe91eb8c912
+size 375042383
diff --git a/ComfyUI/models/style_models/put_t2i_style_model_here b/ComfyUI/models/style_models/put_t2i_style_model_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/ultralytics/bbox/face_yolov8m.pt b/ComfyUI/models/ultralytics/bbox/face_yolov8m.pt
new file mode 100644
index 0000000000000000000000000000000000000000..dbfa5813f1ecf8c0b80c12fc5951a706afdeaf30
--- /dev/null
+++ b/ComfyUI/models/ultralytics/bbox/face_yolov8m.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e3893a92c5c1907136b6cc75404094db767c1e0cfefe1b43e87dad72af2e4c9f
+size 51996128
diff --git a/ComfyUI/models/ultralytics/bbox/hand_yolov8s.pt b/ComfyUI/models/ultralytics/bbox/hand_yolov8s.pt
new file mode 100644
index 0000000000000000000000000000000000000000..0248de16969bce69b7bdf05b6e67373bcb634427
--- /dev/null
+++ b/ComfyUI/models/ultralytics/bbox/hand_yolov8s.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:30878cea9870964d4a238339e9dcff002078bbbaa1a058b07e11c167f67eca1c
+size 22484536
diff --git a/ComfyUI/models/ultralytics/segm/person_yolov8m-seg.pt b/ComfyUI/models/ultralytics/segm/person_yolov8m-seg.pt
new file mode 100644
index 0000000000000000000000000000000000000000..a73627da3336d3910b69f3ed83b65f416a705c5d
--- /dev/null
+++ b/ComfyUI/models/ultralytics/segm/person_yolov8m-seg.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1fd7e562f240a5debd48bf737753de6fb60c63f8664121bb522f090a885d8254
+size 54791722
diff --git a/ComfyUI/models/unet/put_unet_files_here b/ComfyUI/models/unet/put_unet_files_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/upscale_models/put_esrgan_and_other_upscale_models_here b/ComfyUI/models/upscale_models/put_esrgan_and_other_upscale_models_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/vae/orangemix.vae.pt b/ComfyUI/models/vae/orangemix.vae.pt
new file mode 100644
index 0000000000000000000000000000000000000000..0f7b497b5546c1ea8f37609be6efee4d2736105d
--- /dev/null
+++ b/ComfyUI/models/vae/orangemix.vae.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f921fb3f29891d2a77a6571e56b8b5052420d2884129517a333c60b1b4816cdf
+size 822802803
diff --git a/ComfyUI/models/vae/put_vae_here b/ComfyUI/models/vae/put_vae_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/models/vae_approx/put_taesd_encoder_pth_and_taesd_decoder_pth_here b/ComfyUI/models/vae_approx/put_taesd_encoder_pth_and_taesd_decoder_pth_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/nodes.py b/ComfyUI/nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e3ec947cd4e422075d8c16406c0e1b73da88380
--- /dev/null
+++ b/ComfyUI/nodes.py
@@ -0,0 +1,1888 @@
+import torch
+
+import os
+import sys
+import json
+import hashlib
+import traceback
+import math
+import time
+import random
+
+from PIL import Image, ImageOps, ImageSequence
+from PIL.PngImagePlugin import PngInfo
+import numpy as np
+import safetensors.torch
+
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))
+
+
+import comfy.diffusers_load
+import comfy.samplers
+import comfy.sample
+import comfy.sd
+import comfy.utils
+import comfy.controlnet
+
+import comfy.clip_vision
+
+import comfy.model_management
+from comfy.cli_args import args
+
+import importlib
+
+import folder_paths
+import latent_preview
+
+def before_node_execution():
+ comfy.model_management.throw_exception_if_processing_interrupted()
+
+def interrupt_processing(value=True):
+ comfy.model_management.interrupt_current_processing(value)
+
+MAX_RESOLUTION=8192
+
+class CLIPTextEncode:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"text": ("STRING", {"multiline": True}), "clip": ("CLIP", )}}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "encode"
+
+ CATEGORY = "conditioning"
+
+ def encode(self, clip, text):
+ tokens = clip.tokenize(text)
+ cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
+ return ([[cond, {"pooled_output": pooled}]], )
+
+class ConditioningCombine:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning_1": ("CONDITIONING", ), "conditioning_2": ("CONDITIONING", )}}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "combine"
+
+ CATEGORY = "conditioning"
+
+ def combine(self, conditioning_1, conditioning_2):
+ return (conditioning_1 + conditioning_2, )
+
+class ConditioningAverage :
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning_to": ("CONDITIONING", ), "conditioning_from": ("CONDITIONING", ),
+ "conditioning_to_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01})
+ }}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "addWeighted"
+
+ CATEGORY = "conditioning"
+
+ def addWeighted(self, conditioning_to, conditioning_from, conditioning_to_strength):
+ out = []
+
+ if len(conditioning_from) > 1:
+ print("Warning: ConditioningAverage conditioning_from contains more than 1 cond, only the first one will actually be applied to conditioning_to.")
+
+ cond_from = conditioning_from[0][0]
+ pooled_output_from = conditioning_from[0][1].get("pooled_output", None)
+
+ for i in range(len(conditioning_to)):
+ t1 = conditioning_to[i][0]
+ pooled_output_to = conditioning_to[i][1].get("pooled_output", pooled_output_from)
+ t0 = cond_from[:,:t1.shape[1]]
+ if t0.shape[1] < t1.shape[1]:
+ t0 = torch.cat([t0] + [torch.zeros((1, (t1.shape[1] - t0.shape[1]), t1.shape[2]))], dim=1)
+
+ tw = torch.mul(t1, conditioning_to_strength) + torch.mul(t0, (1.0 - conditioning_to_strength))
+ t_to = conditioning_to[i][1].copy()
+ if pooled_output_from is not None and pooled_output_to is not None:
+ t_to["pooled_output"] = torch.mul(pooled_output_to, conditioning_to_strength) + torch.mul(pooled_output_from, (1.0 - conditioning_to_strength))
+ elif pooled_output_from is not None:
+ t_to["pooled_output"] = pooled_output_from
+
+ n = [tw, t_to]
+ out.append(n)
+ return (out, )
+
+class ConditioningConcat:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "conditioning_to": ("CONDITIONING",),
+ "conditioning_from": ("CONDITIONING",),
+ }}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "concat"
+
+ CATEGORY = "conditioning"
+
+ def concat(self, conditioning_to, conditioning_from):
+ out = []
+
+ if len(conditioning_from) > 1:
+ print("Warning: ConditioningConcat conditioning_from contains more than 1 cond, only the first one will actually be applied to conditioning_to.")
+
+ cond_from = conditioning_from[0][0]
+
+ for i in range(len(conditioning_to)):
+ t1 = conditioning_to[i][0]
+ tw = torch.cat((t1, cond_from),1)
+ n = [tw, conditioning_to[i][1].copy()]
+ out.append(n)
+
+ return (out, )
+
+class ConditioningSetArea:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning": ("CONDITIONING", ),
+ "width": ("INT", {"default": 64, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "height": ("INT", {"default": 64, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
+ }}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "append"
+
+ CATEGORY = "conditioning"
+
+ def append(self, conditioning, width, height, x, y, strength):
+ c = []
+ for t in conditioning:
+ n = [t[0], t[1].copy()]
+ n[1]['area'] = (height // 8, width // 8, y // 8, x // 8)
+ n[1]['strength'] = strength
+ n[1]['set_area_to_bounds'] = False
+ c.append(n)
+ return (c, )
+
+class ConditioningSetAreaPercentage:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning": ("CONDITIONING", ),
+ "width": ("FLOAT", {"default": 1.0, "min": 0, "max": 1.0, "step": 0.01}),
+ "height": ("FLOAT", {"default": 1.0, "min": 0, "max": 1.0, "step": 0.01}),
+ "x": ("FLOAT", {"default": 0, "min": 0, "max": 1.0, "step": 0.01}),
+ "y": ("FLOAT", {"default": 0, "min": 0, "max": 1.0, "step": 0.01}),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
+ }}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "append"
+
+ CATEGORY = "conditioning"
+
+ def append(self, conditioning, width, height, x, y, strength):
+ c = []
+ for t in conditioning:
+ n = [t[0], t[1].copy()]
+ n[1]['area'] = ("percentage", height, width, y, x)
+ n[1]['strength'] = strength
+ n[1]['set_area_to_bounds'] = False
+ c.append(n)
+ return (c, )
+
+class ConditioningSetMask:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning": ("CONDITIONING", ),
+ "mask": ("MASK", ),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
+ "set_cond_area": (["default", "mask bounds"],),
+ }}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "append"
+
+ CATEGORY = "conditioning"
+
+ def append(self, conditioning, mask, set_cond_area, strength):
+ c = []
+ set_area_to_bounds = False
+ if set_cond_area != "default":
+ set_area_to_bounds = True
+ if len(mask.shape) < 3:
+ mask = mask.unsqueeze(0)
+ for t in conditioning:
+ n = [t[0], t[1].copy()]
+ _, h, w = mask.shape
+ n[1]['mask'] = mask
+ n[1]['set_area_to_bounds'] = set_area_to_bounds
+ n[1]['mask_strength'] = strength
+ c.append(n)
+ return (c, )
+
+class ConditioningZeroOut:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning": ("CONDITIONING", )}}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "zero_out"
+
+ CATEGORY = "advanced/conditioning"
+
+ def zero_out(self, conditioning):
+ c = []
+ for t in conditioning:
+ d = t[1].copy()
+ if "pooled_output" in d:
+ d["pooled_output"] = torch.zeros_like(d["pooled_output"])
+ n = [torch.zeros_like(t[0]), d]
+ c.append(n)
+ return (c, )
+
+class ConditioningSetTimestepRange:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning": ("CONDITIONING", ),
+ "start": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
+ "end": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})
+ }}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "set_range"
+
+ CATEGORY = "advanced/conditioning"
+
+ def set_range(self, conditioning, start, end):
+ c = []
+ for t in conditioning:
+ d = t[1].copy()
+ d['start_percent'] = start
+ d['end_percent'] = end
+ n = [t[0], d]
+ c.append(n)
+ return (c, )
+
+class VAEDecode:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples": ("LATENT", ), "vae": ("VAE", )}}
+ RETURN_TYPES = ("IMAGE",)
+ FUNCTION = "decode"
+
+ CATEGORY = "latent"
+
+ def decode(self, vae, samples):
+ return (vae.decode(samples["samples"]), )
+
+class VAEDecodeTiled:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"samples": ("LATENT", ), "vae": ("VAE", ),
+ "tile_size": ("INT", {"default": 512, "min": 320, "max": 4096, "step": 64})
+ }}
+ RETURN_TYPES = ("IMAGE",)
+ FUNCTION = "decode"
+
+ CATEGORY = "_for_testing"
+
+ def decode(self, vae, samples, tile_size):
+ return (vae.decode_tiled(samples["samples"], tile_x=tile_size // 8, tile_y=tile_size // 8, ), )
+
+class VAEEncode:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "pixels": ("IMAGE", ), "vae": ("VAE", )}}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "encode"
+
+ CATEGORY = "latent"
+
+ @staticmethod
+ def vae_encode_crop_pixels(pixels):
+ x = (pixels.shape[1] // 8) * 8
+ y = (pixels.shape[2] // 8) * 8
+ if pixels.shape[1] != x or pixels.shape[2] != y:
+ x_offset = (pixels.shape[1] % 8) // 2
+ y_offset = (pixels.shape[2] % 8) // 2
+ pixels = pixels[:, x_offset:x + x_offset, y_offset:y + y_offset, :]
+ return pixels
+
+ def encode(self, vae, pixels):
+ pixels = self.vae_encode_crop_pixels(pixels)
+ t = vae.encode(pixels[:,:,:,:3])
+ return ({"samples":t}, )
+
+class VAEEncodeTiled:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"pixels": ("IMAGE", ), "vae": ("VAE", ),
+ "tile_size": ("INT", {"default": 512, "min": 320, "max": 4096, "step": 64})
+ }}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "encode"
+
+ CATEGORY = "_for_testing"
+
+ def encode(self, vae, pixels, tile_size):
+ pixels = VAEEncode.vae_encode_crop_pixels(pixels)
+ t = vae.encode_tiled(pixels[:,:,:,:3], tile_x=tile_size, tile_y=tile_size, )
+ return ({"samples":t}, )
+
+class VAEEncodeForInpaint:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "pixels": ("IMAGE", ), "vae": ("VAE", ), "mask": ("MASK", ), "grow_mask_by": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}),}}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "encode"
+
+ CATEGORY = "latent/inpaint"
+
+ def encode(self, vae, pixels, mask, grow_mask_by=6):
+ x = (pixels.shape[1] // 8) * 8
+ y = (pixels.shape[2] // 8) * 8
+ mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(pixels.shape[1], pixels.shape[2]), mode="bilinear")
+
+ pixels = pixels.clone()
+ if pixels.shape[1] != x or pixels.shape[2] != y:
+ x_offset = (pixels.shape[1] % 8) // 2
+ y_offset = (pixels.shape[2] % 8) // 2
+ pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:]
+ mask = mask[:,:,x_offset:x + x_offset, y_offset:y + y_offset]
+
+ #grow mask by a few pixels to keep things seamless in latent space
+ if grow_mask_by == 0:
+ mask_erosion = mask
+ else:
+ kernel_tensor = torch.ones((1, 1, grow_mask_by, grow_mask_by))
+ padding = math.ceil((grow_mask_by - 1) / 2)
+
+ mask_erosion = torch.clamp(torch.nn.functional.conv2d(mask.round(), kernel_tensor, padding=padding), 0, 1)
+
+ m = (1.0 - mask.round()).squeeze(1)
+ for i in range(3):
+ pixels[:,:,:,i] -= 0.5
+ pixels[:,:,:,i] *= m
+ pixels[:,:,:,i] += 0.5
+ t = vae.encode(pixels)
+
+ return ({"samples":t, "noise_mask": (mask_erosion[:,:,:x,:y].round())}, )
+
+class SaveLatent:
+ def __init__(self):
+ self.output_dir = folder_paths.get_output_directory()
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples": ("LATENT", ),
+ "filename_prefix": ("STRING", {"default": "latents/ComfyUI"})},
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
+ }
+ RETURN_TYPES = ()
+ FUNCTION = "save"
+
+ OUTPUT_NODE = True
+
+ CATEGORY = "_for_testing"
+
+ def save(self, samples, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
+ full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
+
+ # support save metadata for latent sharing
+ prompt_info = ""
+ if prompt is not None:
+ prompt_info = json.dumps(prompt)
+
+ metadata = None
+ if not args.disable_metadata:
+ metadata = {"prompt": prompt_info}
+ if extra_pnginfo is not None:
+ for x in extra_pnginfo:
+ metadata[x] = json.dumps(extra_pnginfo[x])
+
+ file = f"{filename}_{counter:05}_.latent"
+
+ results = list()
+ results.append({
+ "filename": file,
+ "subfolder": subfolder,
+ "type": "output"
+ })
+
+ file = os.path.join(full_output_folder, file)
+
+ output = {}
+ output["latent_tensor"] = samples["samples"]
+ output["latent_format_version_0"] = torch.tensor([])
+
+ comfy.utils.save_torch_file(output, file, metadata=metadata)
+ return { "ui": { "latents": results } }
+
+
+class LoadLatent:
+ @classmethod
+ def INPUT_TYPES(s):
+ input_dir = folder_paths.get_input_directory()
+ files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)) and f.endswith(".latent")]
+ return {"required": {"latent": [sorted(files), ]}, }
+
+ CATEGORY = "_for_testing"
+
+ RETURN_TYPES = ("LATENT", )
+ FUNCTION = "load"
+
+ def load(self, latent):
+ latent_path = folder_paths.get_annotated_filepath(latent)
+ latent = safetensors.torch.load_file(latent_path, device="cpu")
+ multiplier = 1.0
+ if "latent_format_version_0" not in latent:
+ multiplier = 1.0 / 0.18215
+ samples = {"samples": latent["latent_tensor"].float() * multiplier}
+ return (samples, )
+
+ @classmethod
+ def IS_CHANGED(s, latent):
+ image_path = folder_paths.get_annotated_filepath(latent)
+ m = hashlib.sha256()
+ with open(image_path, 'rb') as f:
+ m.update(f.read())
+ return m.digest().hex()
+
+ @classmethod
+ def VALIDATE_INPUTS(s, latent):
+ if not folder_paths.exists_annotated_filepath(latent):
+ return "Invalid latent file: {}".format(latent)
+ return True
+
+
+class CheckpointLoader:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "config_name": (folder_paths.get_filename_list("configs"), ),
+ "ckpt_name": (folder_paths.get_filename_list("checkpoints"), )}}
+ RETURN_TYPES = ("MODEL", "CLIP", "VAE")
+ FUNCTION = "load_checkpoint"
+
+ CATEGORY = "advanced/loaders"
+
+ def load_checkpoint(self, config_name, ckpt_name, output_vae=True, output_clip=True):
+ config_path = folder_paths.get_full_path("configs", config_name)
+ ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
+ return comfy.sd.load_checkpoint(config_path, ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
+
+class CheckpointLoaderSimple:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),
+ }}
+ RETURN_TYPES = ("MODEL", "CLIP", "VAE")
+ FUNCTION = "load_checkpoint"
+
+ CATEGORY = "loaders"
+
+ def load_checkpoint(self, ckpt_name, output_vae=True, output_clip=True):
+ ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
+ out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
+ return out[:3]
+
+class DiffusersLoader:
+ @classmethod
+ def INPUT_TYPES(cls):
+ paths = []
+ for search_path in folder_paths.get_folder_paths("diffusers"):
+ if os.path.exists(search_path):
+ for root, subdir, files in os.walk(search_path, followlinks=True):
+ if "model_index.json" in files:
+ paths.append(os.path.relpath(root, start=search_path))
+
+ return {"required": {"model_path": (paths,), }}
+ RETURN_TYPES = ("MODEL", "CLIP", "VAE")
+ FUNCTION = "load_checkpoint"
+
+ CATEGORY = "advanced/loaders/deprecated"
+
+ def load_checkpoint(self, model_path, output_vae=True, output_clip=True):
+ for search_path in folder_paths.get_folder_paths("diffusers"):
+ if os.path.exists(search_path):
+ path = os.path.join(search_path, model_path)
+ if os.path.exists(path):
+ model_path = path
+ break
+
+ return comfy.diffusers_load.load_diffusers(model_path, output_vae=output_vae, output_clip=output_clip, embedding_directory=folder_paths.get_folder_paths("embeddings"))
+
+
+class unCLIPCheckpointLoader:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),
+ }}
+ RETURN_TYPES = ("MODEL", "CLIP", "VAE", "CLIP_VISION")
+ FUNCTION = "load_checkpoint"
+
+ CATEGORY = "loaders"
+
+ def load_checkpoint(self, ckpt_name, output_vae=True, output_clip=True):
+ ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
+ out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, output_clipvision=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
+ return out
+
+class CLIPSetLastLayer:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "clip": ("CLIP", ),
+ "stop_at_clip_layer": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}),
+ }}
+ RETURN_TYPES = ("CLIP",)
+ FUNCTION = "set_last_layer"
+
+ CATEGORY = "conditioning"
+
+ def set_last_layer(self, clip, stop_at_clip_layer):
+ clip = clip.clone()
+ clip.clip_layer(stop_at_clip_layer)
+ return (clip,)
+
+class LoraLoader:
+ def __init__(self):
+ self.loaded_lora = None
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "model": ("MODEL",),
+ "clip": ("CLIP", ),
+ "lora_name": (folder_paths.get_filename_list("loras"), ),
+ "strength_model": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01}),
+ "strength_clip": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01}),
+ }}
+ RETURN_TYPES = ("MODEL", "CLIP")
+ FUNCTION = "load_lora"
+
+ CATEGORY = "loaders"
+
+ def load_lora(self, model, clip, lora_name, strength_model, strength_clip):
+ if strength_model == 0 and strength_clip == 0:
+ return (model, clip)
+
+ lora_path = folder_paths.get_full_path("loras", lora_name)
+ lora = None
+ if self.loaded_lora is not None:
+ if self.loaded_lora[0] == lora_path:
+ lora = self.loaded_lora[1]
+ else:
+ temp = self.loaded_lora
+ self.loaded_lora = None
+ del temp
+
+ if lora is None:
+ lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
+ self.loaded_lora = (lora_path, lora)
+
+ model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip)
+ return (model_lora, clip_lora)
+
+class LoraLoaderModelOnly(LoraLoader):
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "model": ("MODEL",),
+ "lora_name": (folder_paths.get_filename_list("loras"), ),
+ "strength_model": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01}),
+ }}
+ RETURN_TYPES = ("MODEL",)
+ FUNCTION = "load_lora_model_only"
+
+ def load_lora_model_only(self, model, lora_name, strength_model):
+ return (self.load_lora(model, None, lora_name, strength_model, 0)[0],)
+
+class VAELoader:
+ @staticmethod
+ def vae_list():
+ vaes = folder_paths.get_filename_list("vae")
+ approx_vaes = folder_paths.get_filename_list("vae_approx")
+ sdxl_taesd_enc = False
+ sdxl_taesd_dec = False
+ sd1_taesd_enc = False
+ sd1_taesd_dec = False
+
+ for v in approx_vaes:
+ if v.startswith("taesd_decoder."):
+ sd1_taesd_dec = True
+ elif v.startswith("taesd_encoder."):
+ sd1_taesd_enc = True
+ elif v.startswith("taesdxl_decoder."):
+ sdxl_taesd_dec = True
+ elif v.startswith("taesdxl_encoder."):
+ sdxl_taesd_enc = True
+ if sd1_taesd_dec and sd1_taesd_enc:
+ vaes.append("taesd")
+ if sdxl_taesd_dec and sdxl_taesd_enc:
+ vaes.append("taesdxl")
+ return vaes
+
+ @staticmethod
+ def load_taesd(name):
+ sd = {}
+ approx_vaes = folder_paths.get_filename_list("vae_approx")
+
+ encoder = next(filter(lambda a: a.startswith("{}_encoder.".format(name)), approx_vaes))
+ decoder = next(filter(lambda a: a.startswith("{}_decoder.".format(name)), approx_vaes))
+
+ enc = comfy.utils.load_torch_file(folder_paths.get_full_path("vae_approx", encoder))
+ for k in enc:
+ sd["taesd_encoder.{}".format(k)] = enc[k]
+
+ dec = comfy.utils.load_torch_file(folder_paths.get_full_path("vae_approx", decoder))
+ for k in dec:
+ sd["taesd_decoder.{}".format(k)] = dec[k]
+
+ if name == "taesd":
+ sd["vae_scale"] = torch.tensor(0.18215)
+ elif name == "taesdxl":
+ sd["vae_scale"] = torch.tensor(0.13025)
+ return sd
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "vae_name": (s.vae_list(), )}}
+ RETURN_TYPES = ("VAE",)
+ FUNCTION = "load_vae"
+
+ CATEGORY = "loaders"
+
+ #TODO: scale factor?
+ def load_vae(self, vae_name):
+ if vae_name in ["taesd", "taesdxl"]:
+ sd = self.load_taesd(vae_name)
+ else:
+ vae_path = folder_paths.get_full_path("vae", vae_name)
+ sd = comfy.utils.load_torch_file(vae_path)
+ vae = comfy.sd.VAE(sd=sd)
+ return (vae,)
+
+class ControlNetLoader:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "control_net_name": (folder_paths.get_filename_list("controlnet"), )}}
+
+ RETURN_TYPES = ("CONTROL_NET",)
+ FUNCTION = "load_controlnet"
+
+ CATEGORY = "loaders"
+
+ def load_controlnet(self, control_net_name):
+ controlnet_path = folder_paths.get_full_path("controlnet", control_net_name)
+ controlnet = comfy.controlnet.load_controlnet(controlnet_path)
+ return (controlnet,)
+
+class DiffControlNetLoader:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "model": ("MODEL",),
+ "control_net_name": (folder_paths.get_filename_list("controlnet"), )}}
+
+ RETURN_TYPES = ("CONTROL_NET",)
+ FUNCTION = "load_controlnet"
+
+ CATEGORY = "loaders"
+
+ def load_controlnet(self, model, control_net_name):
+ controlnet_path = folder_paths.get_full_path("controlnet", control_net_name)
+ controlnet = comfy.controlnet.load_controlnet(controlnet_path, model)
+ return (controlnet,)
+
+
+class ControlNetApply:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning": ("CONDITIONING", ),
+ "control_net": ("CONTROL_NET", ),
+ "image": ("IMAGE", ),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01})
+ }}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "apply_controlnet"
+
+ CATEGORY = "conditioning"
+
+ def apply_controlnet(self, conditioning, control_net, image, strength):
+ if strength == 0:
+ return (conditioning, )
+
+ c = []
+ control_hint = image.movedim(-1,1)
+ for t in conditioning:
+ n = [t[0], t[1].copy()]
+ c_net = control_net.copy().set_cond_hint(control_hint, strength)
+ if 'control' in t[1]:
+ c_net.set_previous_controlnet(t[1]['control'])
+ n[1]['control'] = c_net
+ n[1]['control_apply_to_uncond'] = True
+ c.append(n)
+ return (c, )
+
+
+class ControlNetApplyAdvanced:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"positive": ("CONDITIONING", ),
+ "negative": ("CONDITIONING", ),
+ "control_net": ("CONTROL_NET", ),
+ "image": ("IMAGE", ),
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
+ "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
+ "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})
+ }}
+
+ RETURN_TYPES = ("CONDITIONING","CONDITIONING")
+ RETURN_NAMES = ("positive", "negative")
+ FUNCTION = "apply_controlnet"
+
+ CATEGORY = "conditioning"
+
+ def apply_controlnet(self, positive, negative, control_net, image, strength, start_percent, end_percent):
+ if strength == 0:
+ return (positive, negative)
+
+ control_hint = image.movedim(-1,1)
+ cnets = {}
+
+ out = []
+ for conditioning in [positive, negative]:
+ c = []
+ for t in conditioning:
+ d = t[1].copy()
+
+ prev_cnet = d.get('control', None)
+ if prev_cnet in cnets:
+ c_net = cnets[prev_cnet]
+ else:
+ c_net = control_net.copy().set_cond_hint(control_hint, strength, (start_percent, end_percent))
+ c_net.set_previous_controlnet(prev_cnet)
+ cnets[prev_cnet] = c_net
+
+ d['control'] = c_net
+ d['control_apply_to_uncond'] = False
+ n = [t[0], d]
+ c.append(n)
+ out.append(c)
+ return (out[0], out[1])
+
+
+class UNETLoader:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "unet_name": (folder_paths.get_filename_list("unet"), ),
+ }}
+ RETURN_TYPES = ("MODEL",)
+ FUNCTION = "load_unet"
+
+ CATEGORY = "advanced/loaders"
+
+ def load_unet(self, unet_name):
+ unet_path = folder_paths.get_full_path("unet", unet_name)
+ model = comfy.sd.load_unet(unet_path)
+ return (model,)
+
+class CLIPLoader:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "clip_name": (folder_paths.get_filename_list("clip"), ),
+ }}
+ RETURN_TYPES = ("CLIP",)
+ FUNCTION = "load_clip"
+
+ CATEGORY = "advanced/loaders"
+
+ def load_clip(self, clip_name):
+ clip_path = folder_paths.get_full_path("clip", clip_name)
+ clip = comfy.sd.load_clip(ckpt_paths=[clip_path], embedding_directory=folder_paths.get_folder_paths("embeddings"))
+ return (clip,)
+
+class DualCLIPLoader:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "clip_name1": (folder_paths.get_filename_list("clip"), ), "clip_name2": (folder_paths.get_filename_list("clip"), ),
+ }}
+ RETURN_TYPES = ("CLIP",)
+ FUNCTION = "load_clip"
+
+ CATEGORY = "advanced/loaders"
+
+ def load_clip(self, clip_name1, clip_name2):
+ clip_path1 = folder_paths.get_full_path("clip", clip_name1)
+ clip_path2 = folder_paths.get_full_path("clip", clip_name2)
+ clip = comfy.sd.load_clip(ckpt_paths=[clip_path1, clip_path2], embedding_directory=folder_paths.get_folder_paths("embeddings"))
+ return (clip,)
+
+class CLIPVisionLoader:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "clip_name": (folder_paths.get_filename_list("clip_vision"), ),
+ }}
+ RETURN_TYPES = ("CLIP_VISION",)
+ FUNCTION = "load_clip"
+
+ CATEGORY = "loaders"
+
+ def load_clip(self, clip_name):
+ clip_path = folder_paths.get_full_path("clip_vision", clip_name)
+ clip_vision = comfy.clip_vision.load(clip_path)
+ return (clip_vision,)
+
+class CLIPVisionEncode:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "clip_vision": ("CLIP_VISION",),
+ "image": ("IMAGE",)
+ }}
+ RETURN_TYPES = ("CLIP_VISION_OUTPUT",)
+ FUNCTION = "encode"
+
+ CATEGORY = "conditioning"
+
+ def encode(self, clip_vision, image):
+ output = clip_vision.encode_image(image)
+ return (output,)
+
+class StyleModelLoader:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "style_model_name": (folder_paths.get_filename_list("style_models"), )}}
+
+ RETURN_TYPES = ("STYLE_MODEL",)
+ FUNCTION = "load_style_model"
+
+ CATEGORY = "loaders"
+
+ def load_style_model(self, style_model_name):
+ style_model_path = folder_paths.get_full_path("style_models", style_model_name)
+ style_model = comfy.sd.load_style_model(style_model_path)
+ return (style_model,)
+
+
+class StyleModelApply:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning": ("CONDITIONING", ),
+ "style_model": ("STYLE_MODEL", ),
+ "clip_vision_output": ("CLIP_VISION_OUTPUT", ),
+ }}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "apply_stylemodel"
+
+ CATEGORY = "conditioning/style_model"
+
+ def apply_stylemodel(self, clip_vision_output, style_model, conditioning):
+ cond = style_model.get_cond(clip_vision_output).flatten(start_dim=0, end_dim=1).unsqueeze(dim=0)
+ c = []
+ for t in conditioning:
+ n = [torch.cat((t[0], cond), dim=1), t[1].copy()]
+ c.append(n)
+ return (c, )
+
+class unCLIPConditioning:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning": ("CONDITIONING", ),
+ "clip_vision_output": ("CLIP_VISION_OUTPUT", ),
+ "strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
+ "noise_augmentation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
+ }}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "apply_adm"
+
+ CATEGORY = "conditioning"
+
+ def apply_adm(self, conditioning, clip_vision_output, strength, noise_augmentation):
+ if strength == 0:
+ return (conditioning, )
+
+ c = []
+ for t in conditioning:
+ o = t[1].copy()
+ x = {"clip_vision_output": clip_vision_output, "strength": strength, "noise_augmentation": noise_augmentation}
+ if "unclip_conditioning" in o:
+ o["unclip_conditioning"] = o["unclip_conditioning"][:] + [x]
+ else:
+ o["unclip_conditioning"] = [x]
+ n = [t[0], o]
+ c.append(n)
+ return (c, )
+
+class GLIGENLoader:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "gligen_name": (folder_paths.get_filename_list("gligen"), )}}
+
+ RETURN_TYPES = ("GLIGEN",)
+ FUNCTION = "load_gligen"
+
+ CATEGORY = "loaders"
+
+ def load_gligen(self, gligen_name):
+ gligen_path = folder_paths.get_full_path("gligen", gligen_name)
+ gligen = comfy.sd.load_gligen(gligen_path)
+ return (gligen,)
+
+class GLIGENTextBoxApply:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {"conditioning_to": ("CONDITIONING", ),
+ "clip": ("CLIP", ),
+ "gligen_textbox_model": ("GLIGEN", ),
+ "text": ("STRING", {"multiline": True}),
+ "width": ("INT", {"default": 64, "min": 8, "max": MAX_RESOLUTION, "step": 8}),
+ "height": ("INT", {"default": 64, "min": 8, "max": MAX_RESOLUTION, "step": 8}),
+ "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ }}
+ RETURN_TYPES = ("CONDITIONING",)
+ FUNCTION = "append"
+
+ CATEGORY = "conditioning/gligen"
+
+ def append(self, conditioning_to, clip, gligen_textbox_model, text, width, height, x, y):
+ c = []
+ cond, cond_pooled = clip.encode_from_tokens(clip.tokenize(text), return_pooled=True)
+ for t in conditioning_to:
+ n = [t[0], t[1].copy()]
+ position_params = [(cond_pooled, height // 8, width // 8, y // 8, x // 8)]
+ prev = []
+ if "gligen" in n[1]:
+ prev = n[1]['gligen'][2]
+
+ n[1]['gligen'] = ("position", gligen_textbox_model, prev + position_params)
+ c.append(n)
+ return (c, )
+
+class EmptyLatentImage:
+ def __init__(self):
+ self.device = comfy.model_management.intermediate_device()
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
+ "height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
+ "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096})}}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "generate"
+
+ CATEGORY = "latent"
+
+ def generate(self, width, height, batch_size=1):
+ latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=self.device)
+ return ({"samples":latent}, )
+
+
+class LatentFromBatch:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples": ("LATENT",),
+ "batch_index": ("INT", {"default": 0, "min": 0, "max": 63}),
+ "length": ("INT", {"default": 1, "min": 1, "max": 64}),
+ }}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "frombatch"
+
+ CATEGORY = "latent/batch"
+
+ def frombatch(self, samples, batch_index, length):
+ s = samples.copy()
+ s_in = samples["samples"]
+ batch_index = min(s_in.shape[0] - 1, batch_index)
+ length = min(s_in.shape[0] - batch_index, length)
+ s["samples"] = s_in[batch_index:batch_index + length].clone()
+ if "noise_mask" in samples:
+ masks = samples["noise_mask"]
+ if masks.shape[0] == 1:
+ s["noise_mask"] = masks.clone()
+ else:
+ if masks.shape[0] < s_in.shape[0]:
+ masks = masks.repeat(math.ceil(s_in.shape[0] / masks.shape[0]), 1, 1, 1)[:s_in.shape[0]]
+ s["noise_mask"] = masks[batch_index:batch_index + length].clone()
+ if "batch_index" not in s:
+ s["batch_index"] = [x for x in range(batch_index, batch_index+length)]
+ else:
+ s["batch_index"] = samples["batch_index"][batch_index:batch_index + length]
+ return (s,)
+
+class RepeatLatentBatch:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples": ("LATENT",),
+ "amount": ("INT", {"default": 1, "min": 1, "max": 64}),
+ }}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "repeat"
+
+ CATEGORY = "latent/batch"
+
+ def repeat(self, samples, amount):
+ s = samples.copy()
+ s_in = samples["samples"]
+
+ s["samples"] = s_in.repeat((amount, 1,1,1))
+ if "noise_mask" in samples and samples["noise_mask"].shape[0] > 1:
+ masks = samples["noise_mask"]
+ if masks.shape[0] < s_in.shape[0]:
+ masks = masks.repeat(math.ceil(s_in.shape[0] / masks.shape[0]), 1, 1, 1)[:s_in.shape[0]]
+ s["noise_mask"] = samples["noise_mask"].repeat((amount, 1,1,1))
+ if "batch_index" in s:
+ offset = max(s["batch_index"]) - min(s["batch_index"]) + 1
+ s["batch_index"] = s["batch_index"] + [x + (i * offset) for i in range(1, amount) for x in s["batch_index"]]
+ return (s,)
+
+class LatentUpscale:
+ upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"]
+ crop_methods = ["disabled", "center"]
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples": ("LATENT",), "upscale_method": (s.upscale_methods,),
+ "width": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "height": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "crop": (s.crop_methods,)}}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "upscale"
+
+ CATEGORY = "latent"
+
+ def upscale(self, samples, upscale_method, width, height, crop):
+ if width == 0 and height == 0:
+ s = samples
+ else:
+ s = samples.copy()
+
+ if width == 0:
+ height = max(64, height)
+ width = max(64, round(samples["samples"].shape[3] * height / samples["samples"].shape[2]))
+ elif height == 0:
+ width = max(64, width)
+ height = max(64, round(samples["samples"].shape[2] * width / samples["samples"].shape[3]))
+ else:
+ width = max(64, width)
+ height = max(64, height)
+
+ s["samples"] = comfy.utils.common_upscale(samples["samples"], width // 8, height // 8, upscale_method, crop)
+ return (s,)
+
+class LatentUpscaleBy:
+ upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"]
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples": ("LATENT",), "upscale_method": (s.upscale_methods,),
+ "scale_by": ("FLOAT", {"default": 1.5, "min": 0.01, "max": 8.0, "step": 0.01}),}}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "upscale"
+
+ CATEGORY = "latent"
+
+ def upscale(self, samples, upscale_method, scale_by):
+ s = samples.copy()
+ width = round(samples["samples"].shape[3] * scale_by)
+ height = round(samples["samples"].shape[2] * scale_by)
+ s["samples"] = comfy.utils.common_upscale(samples["samples"], width, height, upscale_method, "disabled")
+ return (s,)
+
+class LatentRotate:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples": ("LATENT",),
+ "rotation": (["none", "90 degrees", "180 degrees", "270 degrees"],),
+ }}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "rotate"
+
+ CATEGORY = "latent/transform"
+
+ def rotate(self, samples, rotation):
+ s = samples.copy()
+ rotate_by = 0
+ if rotation.startswith("90"):
+ rotate_by = 1
+ elif rotation.startswith("180"):
+ rotate_by = 2
+ elif rotation.startswith("270"):
+ rotate_by = 3
+
+ s["samples"] = torch.rot90(samples["samples"], k=rotate_by, dims=[3, 2])
+ return (s,)
+
+class LatentFlip:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples": ("LATENT",),
+ "flip_method": (["x-axis: vertically", "y-axis: horizontally"],),
+ }}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "flip"
+
+ CATEGORY = "latent/transform"
+
+ def flip(self, samples, flip_method):
+ s = samples.copy()
+ if flip_method.startswith("x"):
+ s["samples"] = torch.flip(samples["samples"], dims=[2])
+ elif flip_method.startswith("y"):
+ s["samples"] = torch.flip(samples["samples"], dims=[3])
+
+ return (s,)
+
+class LatentComposite:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples_to": ("LATENT",),
+ "samples_from": ("LATENT",),
+ "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "feather": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ }}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "composite"
+
+ CATEGORY = "latent"
+
+ def composite(self, samples_to, samples_from, x, y, composite_method="normal", feather=0):
+ x = x // 8
+ y = y // 8
+ feather = feather // 8
+ samples_out = samples_to.copy()
+ s = samples_to["samples"].clone()
+ samples_to = samples_to["samples"]
+ samples_from = samples_from["samples"]
+ if feather == 0:
+ s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x]
+ else:
+ samples_from = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x]
+ mask = torch.ones_like(samples_from)
+ for t in range(feather):
+ if y != 0:
+ mask[:,:,t:1+t,:] *= ((1.0/feather) * (t + 1))
+
+ if y + samples_from.shape[2] < samples_to.shape[2]:
+ mask[:,:,mask.shape[2] -1 -t: mask.shape[2]-t,:] *= ((1.0/feather) * (t + 1))
+ if x != 0:
+ mask[:,:,:,t:1+t] *= ((1.0/feather) * (t + 1))
+ if x + samples_from.shape[3] < samples_to.shape[3]:
+ mask[:,:,:,mask.shape[3]- 1 - t: mask.shape[3]- t] *= ((1.0/feather) * (t + 1))
+ rev_mask = torch.ones_like(mask) - mask
+ s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x] * mask + s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] * rev_mask
+ samples_out["samples"] = s
+ return (samples_out,)
+
+class LatentBlend:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": {
+ "samples1": ("LATENT",),
+ "samples2": ("LATENT",),
+ "blend_factor": ("FLOAT", {
+ "default": 0.5,
+ "min": 0,
+ "max": 1,
+ "step": 0.01
+ }),
+ }}
+
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "blend"
+
+ CATEGORY = "_for_testing"
+
+ def blend(self, samples1, samples2, blend_factor:float, blend_mode: str="normal"):
+
+ samples_out = samples1.copy()
+ samples1 = samples1["samples"]
+ samples2 = samples2["samples"]
+
+ if samples1.shape != samples2.shape:
+ samples2.permute(0, 3, 1, 2)
+ samples2 = comfy.utils.common_upscale(samples2, samples1.shape[3], samples1.shape[2], 'bicubic', crop='center')
+ samples2.permute(0, 2, 3, 1)
+
+ samples_blended = self.blend_mode(samples1, samples2, blend_mode)
+ samples_blended = samples1 * blend_factor + samples_blended * (1 - blend_factor)
+ samples_out["samples"] = samples_blended
+ return (samples_out,)
+
+ def blend_mode(self, img1, img2, mode):
+ if mode == "normal":
+ return img2
+ else:
+ raise ValueError(f"Unsupported blend mode: {mode}")
+
+class LatentCrop:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples": ("LATENT",),
+ "width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
+ "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ }}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "crop"
+
+ CATEGORY = "latent/transform"
+
+ def crop(self, samples, width, height, x, y):
+ s = samples.copy()
+ samples = samples['samples']
+ x = x // 8
+ y = y // 8
+
+ #enfonce minimum size of 64
+ if x > (samples.shape[3] - 8):
+ x = samples.shape[3] - 8
+ if y > (samples.shape[2] - 8):
+ y = samples.shape[2] - 8
+
+ new_height = height // 8
+ new_width = width // 8
+ to_x = new_width + x
+ to_y = new_height + y
+ s['samples'] = samples[:,:,y:to_y, x:to_x]
+ return (s,)
+
+class SetLatentNoiseMask:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "samples": ("LATENT",),
+ "mask": ("MASK",),
+ }}
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "set_mask"
+
+ CATEGORY = "latent/inpaint"
+
+ def set_mask(self, samples, mask):
+ s = samples.copy()
+ s["noise_mask"] = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1]))
+ return (s,)
+
+def common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0, disable_noise=False, start_step=None, last_step=None, force_full_denoise=False):
+ latent_image = latent["samples"]
+ if disable_noise:
+ noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
+ else:
+ batch_inds = latent["batch_index"] if "batch_index" in latent else None
+ noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
+
+ noise_mask = None
+ if "noise_mask" in latent:
+ noise_mask = latent["noise_mask"]
+
+ callback = latent_preview.prepare_callback(model, steps)
+ disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED
+ samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
+ denoise=denoise, disable_noise=disable_noise, start_step=start_step, last_step=last_step,
+ force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=seed)
+ out = latent.copy()
+ out["samples"] = samples
+ return (out, )
+
+class KSampler:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required":
+ {"model": ("MODEL",),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
+ "positive": ("CONDITIONING", ),
+ "negative": ("CONDITIONING", ),
+ "latent_image": ("LATENT", ),
+ "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
+ }
+ }
+
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "sample"
+
+ CATEGORY = "sampling"
+
+ def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=1.0):
+ return common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise)
+
+class KSamplerAdvanced:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required":
+ {"model": ("MODEL",),
+ "add_noise": (["enable", "disable"], ),
+ "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
+ "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
+ "positive": ("CONDITIONING", ),
+ "negative": ("CONDITIONING", ),
+ "latent_image": ("LATENT", ),
+ "start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
+ "end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
+ "return_with_leftover_noise": (["disable", "enable"], ),
+ }
+ }
+
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "sample"
+
+ CATEGORY = "sampling"
+
+ def sample(self, model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, return_with_leftover_noise, denoise=1.0):
+ force_full_denoise = True
+ if return_with_leftover_noise == "enable":
+ force_full_denoise = False
+ disable_noise = False
+ if add_noise == "disable":
+ disable_noise = True
+ return common_ksampler(model, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise)
+
+class SaveImage:
+ def __init__(self):
+ self.output_dir = folder_paths.get_output_directory()
+ self.type = "output"
+ self.prefix_append = ""
+ self.compress_level = 4
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required":
+ {"images": ("IMAGE", ),
+ "filename_prefix": ("STRING", {"default": "ComfyUI"})},
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
+ }
+
+ RETURN_TYPES = ()
+ FUNCTION = "save_images"
+
+ OUTPUT_NODE = True
+
+ CATEGORY = "image"
+
+ def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
+ filename_prefix += self.prefix_append
+ full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
+ results = list()
+ for image in images:
+ i = 255. * image.cpu().numpy()
+ img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
+ metadata = None
+ if not args.disable_metadata:
+ metadata = PngInfo()
+ if prompt is not None:
+ metadata.add_text("prompt", json.dumps(prompt))
+ if extra_pnginfo is not None:
+ for x in extra_pnginfo:
+ metadata.add_text(x, json.dumps(extra_pnginfo[x]))
+
+ file = f"{filename}_{counter:05}_.png"
+ img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level)
+ results.append({
+ "filename": file,
+ "subfolder": subfolder,
+ "type": self.type
+ })
+ counter += 1
+
+ return { "ui": { "images": results } }
+
+class PreviewImage(SaveImage):
+ def __init__(self):
+ self.output_dir = folder_paths.get_temp_directory()
+ self.type = "temp"
+ self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
+ self.compress_level = 1
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required":
+ {"images": ("IMAGE", ), },
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
+ }
+
+class LoadImage:
+ @classmethod
+ def INPUT_TYPES(s):
+ input_dir = folder_paths.get_input_directory()
+ files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
+ return {"required":
+ {"image": (sorted(files), {"image_upload": True})},
+ }
+
+ CATEGORY = "image"
+
+ RETURN_TYPES = ("IMAGE", "MASK")
+ FUNCTION = "load_image"
+ def load_image(self, image):
+ image_path = folder_paths.get_annotated_filepath(image)
+ img = Image.open(image_path)
+ output_images = []
+ output_masks = []
+ for i in ImageSequence.Iterator(img):
+ i = ImageOps.exif_transpose(i)
+ image = i.convert("RGB")
+ image = np.array(image).astype(np.float32) / 255.0
+ image = torch.from_numpy(image)[None,]
+ if 'A' in i.getbands():
+ mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
+ mask = 1. - torch.from_numpy(mask)
+ else:
+ mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
+ output_images.append(image)
+ output_masks.append(mask.unsqueeze(0))
+
+ if len(output_images) > 1:
+ output_image = torch.cat(output_images, dim=0)
+ output_mask = torch.cat(output_masks, dim=0)
+ else:
+ output_image = output_images[0]
+ output_mask = output_masks[0]
+
+ return (output_image, output_mask)
+
+ @classmethod
+ def IS_CHANGED(s, image):
+ image_path = folder_paths.get_annotated_filepath(image)
+ m = hashlib.sha256()
+ with open(image_path, 'rb') as f:
+ m.update(f.read())
+ return m.digest().hex()
+
+ @classmethod
+ def VALIDATE_INPUTS(s, image):
+ if not folder_paths.exists_annotated_filepath(image):
+ return "Invalid image file: {}".format(image)
+
+ return True
+
+class LoadImageMask:
+ _color_channels = ["alpha", "red", "green", "blue"]
+ @classmethod
+ def INPUT_TYPES(s):
+ input_dir = folder_paths.get_input_directory()
+ files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
+ return {"required":
+ {"image": (sorted(files), {"image_upload": True}),
+ "channel": (s._color_channels, ), }
+ }
+
+ CATEGORY = "mask"
+
+ RETURN_TYPES = ("MASK",)
+ FUNCTION = "load_image"
+ def load_image(self, image, channel):
+ image_path = folder_paths.get_annotated_filepath(image)
+ i = Image.open(image_path)
+ i = ImageOps.exif_transpose(i)
+ if i.getbands() != ("R", "G", "B", "A"):
+ i = i.convert("RGBA")
+ mask = None
+ c = channel[0].upper()
+ if c in i.getbands():
+ mask = np.array(i.getchannel(c)).astype(np.float32) / 255.0
+ mask = torch.from_numpy(mask)
+ if c == 'A':
+ mask = 1. - mask
+ else:
+ mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
+ return (mask.unsqueeze(0),)
+
+ @classmethod
+ def IS_CHANGED(s, image, channel):
+ image_path = folder_paths.get_annotated_filepath(image)
+ m = hashlib.sha256()
+ with open(image_path, 'rb') as f:
+ m.update(f.read())
+ return m.digest().hex()
+
+ @classmethod
+ def VALIDATE_INPUTS(s, image):
+ if not folder_paths.exists_annotated_filepath(image):
+ return "Invalid image file: {}".format(image)
+
+ return True
+
+class ImageScale:
+ upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
+ crop_methods = ["disabled", "center"]
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "image": ("IMAGE",), "upscale_method": (s.upscale_methods,),
+ "width": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
+ "height": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
+ "crop": (s.crop_methods,)}}
+ RETURN_TYPES = ("IMAGE",)
+ FUNCTION = "upscale"
+
+ CATEGORY = "image/upscaling"
+
+ def upscale(self, image, upscale_method, width, height, crop):
+ if width == 0 and height == 0:
+ s = image
+ else:
+ samples = image.movedim(-1,1)
+
+ if width == 0:
+ width = max(1, round(samples.shape[3] * height / samples.shape[2]))
+ elif height == 0:
+ height = max(1, round(samples.shape[2] * width / samples.shape[3]))
+
+ s = comfy.utils.common_upscale(samples, width, height, upscale_method, crop)
+ s = s.movedim(1,-1)
+ return (s,)
+
+class ImageScaleBy:
+ upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "image": ("IMAGE",), "upscale_method": (s.upscale_methods,),
+ "scale_by": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 8.0, "step": 0.01}),}}
+ RETURN_TYPES = ("IMAGE",)
+ FUNCTION = "upscale"
+
+ CATEGORY = "image/upscaling"
+
+ def upscale(self, image, upscale_method, scale_by):
+ samples = image.movedim(-1,1)
+ width = round(samples.shape[3] * scale_by)
+ height = round(samples.shape[2] * scale_by)
+ s = comfy.utils.common_upscale(samples, width, height, upscale_method, "disabled")
+ s = s.movedim(1,-1)
+ return (s,)
+
+class ImageInvert:
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "image": ("IMAGE",)}}
+
+ RETURN_TYPES = ("IMAGE",)
+ FUNCTION = "invert"
+
+ CATEGORY = "image"
+
+ def invert(self, image):
+ s = 1.0 - image
+ return (s,)
+
+class ImageBatch:
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "image1": ("IMAGE",), "image2": ("IMAGE",)}}
+
+ RETURN_TYPES = ("IMAGE",)
+ FUNCTION = "batch"
+
+ CATEGORY = "image"
+
+ def batch(self, image1, image2):
+ if image1.shape[1:] != image2.shape[1:]:
+ image2 = comfy.utils.common_upscale(image2.movedim(-1,1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1,-1)
+ s = torch.cat((image1, image2), dim=0)
+ return (s,)
+
+class EmptyImage:
+ def __init__(self, device="cpu"):
+ self.device = device
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
+ "height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
+ "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
+ "color": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFF, "step": 1, "display": "color"}),
+ }}
+ RETURN_TYPES = ("IMAGE",)
+ FUNCTION = "generate"
+
+ CATEGORY = "image"
+
+ def generate(self, width, height, batch_size=1, color=0):
+ r = torch.full([batch_size, height, width, 1], ((color >> 16) & 0xFF) / 0xFF)
+ g = torch.full([batch_size, height, width, 1], ((color >> 8) & 0xFF) / 0xFF)
+ b = torch.full([batch_size, height, width, 1], ((color) & 0xFF) / 0xFF)
+ return (torch.cat((r, g, b), dim=-1), )
+
+class ImagePadForOutpaint:
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "image": ("IMAGE",),
+ "left": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "top": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "right": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "bottom": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
+ "feathering": ("INT", {"default": 40, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE", "MASK")
+ FUNCTION = "expand_image"
+
+ CATEGORY = "image"
+
+ def expand_image(self, image, left, top, right, bottom, feathering):
+ d1, d2, d3, d4 = image.size()
+
+ new_image = torch.zeros(
+ (d1, d2 + top + bottom, d3 + left + right, d4),
+ dtype=torch.float32,
+ )
+ new_image[:, top:top + d2, left:left + d3, :] = image
+
+ mask = torch.ones(
+ (d2 + top + bottom, d3 + left + right),
+ dtype=torch.float32,
+ )
+
+ t = torch.zeros(
+ (d2, d3),
+ dtype=torch.float32
+ )
+
+ if feathering > 0 and feathering * 2 < d2 and feathering * 2 < d3:
+
+ for i in range(d2):
+ for j in range(d3):
+ dt = i if top != 0 else d2
+ db = d2 - i if bottom != 0 else d2
+
+ dl = j if left != 0 else d3
+ dr = d3 - j if right != 0 else d3
+
+ d = min(dt, db, dl, dr)
+
+ if d >= feathering:
+ continue
+
+ v = (feathering - d) / feathering
+
+ t[i, j] = v * v
+
+ mask[top:top + d2, left:left + d3] = t
+
+ return (new_image, mask)
+
+
+NODE_CLASS_MAPPINGS = {
+ "KSampler": KSampler,
+ "CheckpointLoaderSimple": CheckpointLoaderSimple,
+ "CLIPTextEncode": CLIPTextEncode,
+ "CLIPSetLastLayer": CLIPSetLastLayer,
+ "VAEDecode": VAEDecode,
+ "VAEEncode": VAEEncode,
+ "VAEEncodeForInpaint": VAEEncodeForInpaint,
+ "VAELoader": VAELoader,
+ "EmptyLatentImage": EmptyLatentImage,
+ "LatentUpscale": LatentUpscale,
+ "LatentUpscaleBy": LatentUpscaleBy,
+ "LatentFromBatch": LatentFromBatch,
+ "RepeatLatentBatch": RepeatLatentBatch,
+ "SaveImage": SaveImage,
+ "PreviewImage": PreviewImage,
+ "LoadImage": LoadImage,
+ "LoadImageMask": LoadImageMask,
+ "ImageScale": ImageScale,
+ "ImageScaleBy": ImageScaleBy,
+ "ImageInvert": ImageInvert,
+ "ImageBatch": ImageBatch,
+ "ImagePadForOutpaint": ImagePadForOutpaint,
+ "EmptyImage": EmptyImage,
+ "ConditioningAverage": ConditioningAverage ,
+ "ConditioningCombine": ConditioningCombine,
+ "ConditioningConcat": ConditioningConcat,
+ "ConditioningSetArea": ConditioningSetArea,
+ "ConditioningSetAreaPercentage": ConditioningSetAreaPercentage,
+ "ConditioningSetMask": ConditioningSetMask,
+ "KSamplerAdvanced": KSamplerAdvanced,
+ "SetLatentNoiseMask": SetLatentNoiseMask,
+ "LatentComposite": LatentComposite,
+ "LatentBlend": LatentBlend,
+ "LatentRotate": LatentRotate,
+ "LatentFlip": LatentFlip,
+ "LatentCrop": LatentCrop,
+ "LoraLoader": LoraLoader,
+ "CLIPLoader": CLIPLoader,
+ "UNETLoader": UNETLoader,
+ "DualCLIPLoader": DualCLIPLoader,
+ "CLIPVisionEncode": CLIPVisionEncode,
+ "StyleModelApply": StyleModelApply,
+ "unCLIPConditioning": unCLIPConditioning,
+ "ControlNetApply": ControlNetApply,
+ "ControlNetApplyAdvanced": ControlNetApplyAdvanced,
+ "ControlNetLoader": ControlNetLoader,
+ "DiffControlNetLoader": DiffControlNetLoader,
+ "StyleModelLoader": StyleModelLoader,
+ "CLIPVisionLoader": CLIPVisionLoader,
+ "VAEDecodeTiled": VAEDecodeTiled,
+ "VAEEncodeTiled": VAEEncodeTiled,
+ "unCLIPCheckpointLoader": unCLIPCheckpointLoader,
+ "GLIGENLoader": GLIGENLoader,
+ "GLIGENTextBoxApply": GLIGENTextBoxApply,
+
+ "CheckpointLoader": CheckpointLoader,
+ "DiffusersLoader": DiffusersLoader,
+
+ "LoadLatent": LoadLatent,
+ "SaveLatent": SaveLatent,
+
+ "ConditioningZeroOut": ConditioningZeroOut,
+ "ConditioningSetTimestepRange": ConditioningSetTimestepRange,
+ "LoraLoaderModelOnly": LoraLoaderModelOnly,
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ # Sampling
+ "KSampler": "KSampler",
+ "KSamplerAdvanced": "KSampler (Advanced)",
+ # Loaders
+ "CheckpointLoader": "Load Checkpoint With Config (DEPRECATED)",
+ "CheckpointLoaderSimple": "Load Checkpoint",
+ "VAELoader": "Load VAE",
+ "LoraLoader": "Load LoRA",
+ "CLIPLoader": "Load CLIP",
+ "ControlNetLoader": "Load ControlNet Model",
+ "DiffControlNetLoader": "Load ControlNet Model (diff)",
+ "StyleModelLoader": "Load Style Model",
+ "CLIPVisionLoader": "Load CLIP Vision",
+ "UpscaleModelLoader": "Load Upscale Model",
+ # Conditioning
+ "CLIPVisionEncode": "CLIP Vision Encode",
+ "StyleModelApply": "Apply Style Model",
+ "CLIPTextEncode": "CLIP Text Encode (Prompt)",
+ "CLIPSetLastLayer": "CLIP Set Last Layer",
+ "ConditioningCombine": "Conditioning (Combine)",
+ "ConditioningAverage ": "Conditioning (Average)",
+ "ConditioningConcat": "Conditioning (Concat)",
+ "ConditioningSetArea": "Conditioning (Set Area)",
+ "ConditioningSetAreaPercentage": "Conditioning (Set Area with Percentage)",
+ "ConditioningSetMask": "Conditioning (Set Mask)",
+ "ControlNetApply": "Apply ControlNet",
+ "ControlNetApplyAdvanced": "Apply ControlNet (Advanced)",
+ # Latent
+ "VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
+ "SetLatentNoiseMask": "Set Latent Noise Mask",
+ "VAEDecode": "VAE Decode",
+ "VAEEncode": "VAE Encode",
+ "LatentRotate": "Rotate Latent",
+ "LatentFlip": "Flip Latent",
+ "LatentCrop": "Crop Latent",
+ "EmptyLatentImage": "Empty Latent Image",
+ "LatentUpscale": "Upscale Latent",
+ "LatentUpscaleBy": "Upscale Latent By",
+ "LatentComposite": "Latent Composite",
+ "LatentBlend": "Latent Blend",
+ "LatentFromBatch" : "Latent From Batch",
+ "RepeatLatentBatch": "Repeat Latent Batch",
+ # Image
+ "SaveImage": "Save Image",
+ "PreviewImage": "Preview Image",
+ "LoadImage": "Load Image",
+ "LoadImageMask": "Load Image (as Mask)",
+ "ImageScale": "Upscale Image",
+ "ImageScaleBy": "Upscale Image By",
+ "ImageUpscaleWithModel": "Upscale Image (using Model)",
+ "ImageInvert": "Invert Image",
+ "ImagePadForOutpaint": "Pad Image for Outpainting",
+ "ImageBatch": "Batch Images",
+ # _for_testing
+ "VAEDecodeTiled": "VAE Decode (Tiled)",
+ "VAEEncodeTiled": "VAE Encode (Tiled)",
+}
+
+EXTENSION_WEB_DIRS = {}
+
+def load_custom_node(module_path, ignore=set()):
+ module_name = os.path.basename(module_path)
+ if os.path.isfile(module_path):
+ sp = os.path.splitext(module_path)
+ module_name = sp[0]
+ try:
+ if os.path.isfile(module_path):
+ module_spec = importlib.util.spec_from_file_location(module_name, module_path)
+ module_dir = os.path.split(module_path)[0]
+ else:
+ module_spec = importlib.util.spec_from_file_location(module_name, os.path.join(module_path, "__init__.py"))
+ module_dir = module_path
+
+ module = importlib.util.module_from_spec(module_spec)
+ sys.modules[module_name] = module
+ module_spec.loader.exec_module(module)
+
+ if hasattr(module, "WEB_DIRECTORY") and getattr(module, "WEB_DIRECTORY") is not None:
+ web_dir = os.path.abspath(os.path.join(module_dir, getattr(module, "WEB_DIRECTORY")))
+ if os.path.isdir(web_dir):
+ EXTENSION_WEB_DIRS[module_name] = web_dir
+
+ if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None:
+ for name in module.NODE_CLASS_MAPPINGS:
+ if name not in ignore:
+ NODE_CLASS_MAPPINGS[name] = module.NODE_CLASS_MAPPINGS[name]
+ if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS") and getattr(module, "NODE_DISPLAY_NAME_MAPPINGS") is not None:
+ NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
+ return True
+ else:
+ print(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS.")
+ return False
+ except Exception as e:
+ print(traceback.format_exc())
+ print(f"Cannot import {module_path} module for custom nodes:", e)
+ return False
+
+def load_custom_nodes():
+ base_node_names = set(NODE_CLASS_MAPPINGS.keys())
+ node_paths = folder_paths.get_folder_paths("custom_nodes")
+ node_import_times = []
+ for custom_node_path in node_paths:
+ possible_modules = os.listdir(os.path.realpath(custom_node_path))
+ if "__pycache__" in possible_modules:
+ possible_modules.remove("__pycache__")
+
+ for possible_module in possible_modules:
+ module_path = os.path.join(custom_node_path, possible_module)
+ if os.path.isfile(module_path) and os.path.splitext(module_path)[1] != ".py": continue
+ if module_path.endswith(".disabled"): continue
+ time_before = time.perf_counter()
+ success = load_custom_node(module_path, base_node_names)
+ node_import_times.append((time.perf_counter() - time_before, module_path, success))
+
+ if len(node_import_times) > 0:
+ print("\nImport times for custom nodes:")
+ for n in sorted(node_import_times):
+ if n[2]:
+ import_message = ""
+ else:
+ import_message = " (IMPORT FAILED)"
+ print("{:6.1f} seconds{}:".format(n[0], import_message), n[1])
+ print()
+
+def init_custom_nodes():
+ extras_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras")
+ extras_files = [
+ "nodes_latent.py",
+ "nodes_hypernetwork.py",
+ "nodes_upscale_model.py",
+ "nodes_post_processing.py",
+ "nodes_mask.py",
+ "nodes_compositing.py",
+ "nodes_rebatch.py",
+ "nodes_model_merging.py",
+ "nodes_tomesd.py",
+ "nodes_clip_sdxl.py",
+ "nodes_canny.py",
+ "nodes_freelunch.py",
+ "nodes_custom_sampler.py",
+ "nodes_hypertile.py",
+ "nodes_model_advanced.py",
+ "nodes_model_downscale.py",
+ "nodes_images.py",
+ "nodes_video_model.py",
+ "nodes_sag.py",
+ "nodes_perpneg.py",
+ "nodes_stable3d.py",
+ ]
+
+ for node_file in extras_files:
+ load_custom_node(os.path.join(extras_dir, node_file))
+
+ load_custom_nodes()
diff --git a/ComfyUI/notebooks/comfyui_colab.ipynb b/ComfyUI/notebooks/comfyui_colab.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..ec83265b42cf88125c0c6c646df5986395a96c77
--- /dev/null
+++ b/ComfyUI/notebooks/comfyui_colab.ipynb
@@ -0,0 +1,329 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "aaaaaaaaaa"
+ },
+ "source": [
+ "Git clone the repo and install the requirements. (ignore the pip errors about protobuf)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "bbbbbbbbbb"
+ },
+ "outputs": [],
+ "source": [
+ "#@title Environment Setup\n",
+ "\n",
+ "from pathlib import Path\n",
+ "\n",
+ "OPTIONS = {}\n",
+ "\n",
+ "USE_GOOGLE_DRIVE = False #@param {type:\"boolean\"}\n",
+ "UPDATE_COMFY_UI = True #@param {type:\"boolean\"}\n",
+ "WORKSPACE = 'ComfyUI'\n",
+ "OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE\n",
+ "OPTIONS['UPDATE_COMFY_UI'] = UPDATE_COMFY_UI\n",
+ "\n",
+ "if OPTIONS['USE_GOOGLE_DRIVE']:\n",
+ " !echo \"Mounting Google Drive...\"\n",
+ " %cd /\n",
+ " \n",
+ " from google.colab import drive\n",
+ " drive.mount('/content/drive')\n",
+ "\n",
+ " WORKSPACE = \"/content/drive/MyDrive/ComfyUI\"\n",
+ " %cd /content/drive/MyDrive\n",
+ "\n",
+ "![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n",
+ "%cd $WORKSPACE\n",
+ "\n",
+ "if OPTIONS['UPDATE_COMFY_UI']:\n",
+ " !echo -= Updating ComfyUI =-\n",
+ " !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/cu121 --extra-index-url https://download.pytorch.org/whl/cu118 --extra-index-url https://download.pytorch.org/whl/cu117"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "cccccccccc"
+ },
+ "source": [
+ "Download some models/checkpoints/vae or custom comfyui nodes (uncomment the commands for the ones you want)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "dddddddddd"
+ },
+ "outputs": [],
+ "source": [
+ "# Checkpoints\n",
+ "\n",
+ "### SDXL\n",
+ "### I recommend these workflow examples: https://comfyanonymous.github.io/ComfyUI_examples/sdxl/\n",
+ "\n",
+ "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "# SDXL ReVision\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors -P ./models/clip_vision/\n",
+ "\n",
+ "# SD1.5\n",
+ "!wget -c https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt -P ./models/checkpoints/\n",
+ "\n",
+ "# SD2\n",
+ "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-ema-pruned.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "# Some SD1.5 anime style\n",
+ "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1_orangemixs.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A3_orangemixs.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/Linaqruf/anything-v3.0/resolve/main/anything-v3-fp16-pruned.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "# Waifu Diffusion 1.5 (anime style SD2.x 768-v)\n",
+ "#!wget -c https://huggingface.co/waifu-diffusion/wd-1-5-beta3/resolve/main/wd-illusion-fp16.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "\n",
+ "# unCLIP models\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/illuminatiDiffusionV1_v11_unCLIP/resolve/main/illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/wd-1.5-beta2_unCLIP/resolve/main/wd-1-5-beta2-aesthetic-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "\n",
+ "# VAE\n",
+ "!wget -c https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors -P ./models/vae/\n",
+ "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt -P ./models/vae/\n",
+ "#!wget -c https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt -P ./models/vae/\n",
+ "\n",
+ "\n",
+ "# Loras\n",
+ "#!wget -c https://civitai.com/api/download/models/10350 -O ./models/loras/theovercomer8sContrastFix_sd21768.safetensors #theovercomer8sContrastFix SD2.x 768-v\n",
+ "#!wget -c https://civitai.com/api/download/models/10638 -O ./models/loras/theovercomer8sContrastFix_sd15.safetensors #theovercomer8sContrastFix SD1.x\n",
+ "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_offset_example-lora_1.0.safetensors -P ./models/loras/ #SDXL offset noise lora\n",
+ "\n",
+ "\n",
+ "# T2I-Adapter\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_seg_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_sketch_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_keypose_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_openpose_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_color_sd14v1.pth -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_canny_sd14v1.pth -P ./models/controlnet/\n",
+ "\n",
+ "# T2I Styles Model\n",
+ "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_style_sd14v1.pth -P ./models/style_models/\n",
+ "\n",
+ "# CLIPVision model (needed for styles model)\n",
+ "#!wget -c https://huggingface.co/openai/clip-vit-large-patch14/resolve/main/pytorch_model.bin -O ./models/clip_vision/clip_vit14.bin\n",
+ "\n",
+ "\n",
+ "# ControlNet\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_ip2p_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_shuffle_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_canny_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11f1p_sd15_depth_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_inpaint_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_lineart_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_mlsd_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_normalbae_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_seg_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_softedge_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15s2_lineart_anime_fp16.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11u_sd15_tile_fp16.safetensors -P ./models/controlnet/\n",
+ "\n",
+ "# ControlNet SDXL\n",
+ "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-canny-rank256.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-depth-rank256.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-recolor-rank256.safetensors -P ./models/controlnet/\n",
+ "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors -P ./models/controlnet/\n",
+ "\n",
+ "# Controlnet Preprocessor nodes by Fannovel16\n",
+ "#!cd custom_nodes && git clone https://github.com/Fannovel16/comfy_controlnet_preprocessors; cd comfy_controlnet_preprocessors && python install.py\n",
+ "\n",
+ "\n",
+ "# GLIGEN\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/GLIGEN_pruned_safetensors/resolve/main/gligen_sd14_textbox_pruned_fp16.safetensors -P ./models/gligen/\n",
+ "\n",
+ "\n",
+ "# ESRGAN upscale model\n",
+ "#!wget -c https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth -P ./models/upscale_models/\n",
+ "#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x2.pth -P ./models/upscale_models/\n",
+ "#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x4.pth -P ./models/upscale_models/\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "kkkkkkkkkkkkkkk"
+ },
+ "source": [
+ "### Run ComfyUI with cloudflared (Recommended Way)\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "jjjjjjjjjjjjjj"
+ },
+ "outputs": [],
+ "source": [
+ "!wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n",
+ "!dpkg -i cloudflared-linux-amd64.deb\n",
+ "\n",
+ "import subprocess\n",
+ "import threading\n",
+ "import time\n",
+ "import socket\n",
+ "import urllib.request\n",
+ "\n",
+ "def iframe_thread(port):\n",
+ " while True:\n",
+ " time.sleep(0.5)\n",
+ " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
+ " result = sock.connect_ex(('127.0.0.1', port))\n",
+ " if result == 0:\n",
+ " break\n",
+ " sock.close()\n",
+ " print(\"\\nComfyUI finished loading, trying to launch cloudflared (if it gets stuck here cloudflared is having issues)\\n\")\n",
+ "\n",
+ " p = subprocess.Popen([\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:{}\".format(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " for line in p.stderr:\n",
+ " l = line.decode()\n",
+ " if \"trycloudflare.com \" in l:\n",
+ " print(\"This is the URL to access ComfyUI:\", l[l.find(\"http\"):], end='')\n",
+ " #print(l, end='')\n",
+ "\n",
+ "\n",
+ "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
+ "\n",
+ "!python main.py --dont-print-server"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "kkkkkkkkkkkkkk"
+ },
+ "source": [
+ "### Run ComfyUI with localtunnel\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "jjjjjjjjjjjjj"
+ },
+ "outputs": [],
+ "source": [
+ "!npm install -g localtunnel\n",
+ "\n",
+ "import subprocess\n",
+ "import threading\n",
+ "import time\n",
+ "import socket\n",
+ "import urllib.request\n",
+ "\n",
+ "def iframe_thread(port):\n",
+ " while True:\n",
+ " time.sleep(0.5)\n",
+ " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
+ " result = sock.connect_ex(('127.0.0.1', port))\n",
+ " if result == 0:\n",
+ " break\n",
+ " sock.close()\n",
+ " print(\"\\nComfyUI finished loading, trying to launch localtunnel (if it gets stuck here localtunnel is having issues)\\n\")\n",
+ "\n",
+ " print(\"The password/enpoint ip for localtunnel is:\", urllib.request.urlopen('https://ipv4.icanhazip.com').read().decode('utf8').strip(\"\\n\"))\n",
+ " p = subprocess.Popen([\"lt\", \"--port\", \"{}\".format(port)], stdout=subprocess.PIPE)\n",
+ " for line in p.stdout:\n",
+ " print(line.decode(), end='')\n",
+ "\n",
+ "\n",
+ "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
+ "\n",
+ "!python main.py --dont-print-server"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "gggggggggg"
+ },
+ "source": [
+ "### Run ComfyUI with colab iframe (use only in case the previous way with localtunnel doesn't work)\n",
+ "\n",
+ "You should see the ui appear in an iframe. If you get a 403 error, it's your firefox settings or an extension that's messing things up.\n",
+ "\n",
+ "If you want to open it in another window use the link.\n",
+ "\n",
+ "Note that some UI features like live image previews won't work because the colab iframe blocks websockets."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "hhhhhhhhhh"
+ },
+ "outputs": [],
+ "source": [
+ "import threading\n",
+ "import time\n",
+ "import socket\n",
+ "def iframe_thread(port):\n",
+ " while True:\n",
+ " time.sleep(0.5)\n",
+ " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
+ " result = sock.connect_ex(('127.0.0.1', port))\n",
+ " if result == 0:\n",
+ " break\n",
+ " sock.close()\n",
+ " from google.colab import output\n",
+ " output.serve_kernel_port_as_iframe(port, height=1024)\n",
+ " print(\"to open it in a window you can open this link here:\")\n",
+ " output.serve_kernel_port_as_window(port)\n",
+ "\n",
+ "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
+ "\n",
+ "!python main.py --dont-print-server"
+ ]
+ }
+ ],
+ "metadata": {
+ "accelerator": "GPU",
+ "colab": {
+ "provenance": []
+ },
+ "gpuClass": "standard",
+ "kernelspec": {
+ "display_name": "Python 3",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/ComfyUI/output/ComfyUI_00001_.webp b/ComfyUI/output/ComfyUI_00001_.webp
new file mode 100644
index 0000000000000000000000000000000000000000..527fc158e8c02a7d9259cf62806497b4da1fd46f
--- /dev/null
+++ b/ComfyUI/output/ComfyUI_00001_.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:68720dccb451e5b61d237ccc194a054a5ddc426a4c1a74822bdcbbe9894d3380
+size 3006610
diff --git a/ComfyUI/output/ComfyUI_00002_.webp b/ComfyUI/output/ComfyUI_00002_.webp
new file mode 100644
index 0000000000000000000000000000000000000000..0034834e293dc0338c920932b0c0c6ffcf2626fa
--- /dev/null
+++ b/ComfyUI/output/ComfyUI_00002_.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a611eec4c423870e46928419c10e3365c0ddc98c89de6802f6dd617ed01eb16b
+size 2841604
diff --git a/ComfyUI/output/ComfyUI_00003_.webp b/ComfyUI/output/ComfyUI_00003_.webp
new file mode 100644
index 0000000000000000000000000000000000000000..4a1f29f3efa07145fe18f0e726ac55a691f8d430
--- /dev/null
+++ b/ComfyUI/output/ComfyUI_00003_.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0860ddd2841910d62cbfe67bc52301ef97ec0380c60168b0e1ad7339e0aec1ce
+size 2209476
diff --git a/ComfyUI/output/ComfyUI_00004_.webp b/ComfyUI/output/ComfyUI_00004_.webp
new file mode 100644
index 0000000000000000000000000000000000000000..f5c355dafc163e7368c373a64476d1c87cb4ef47
--- /dev/null
+++ b/ComfyUI/output/ComfyUI_00004_.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:25426cb561c692b46f1678a6c9775c82388ef8d5ced0b4ac91972d274e2e7c10
+size 3756622
diff --git a/ComfyUI/output/ComfyUI_00005_.webp b/ComfyUI/output/ComfyUI_00005_.webp
new file mode 100644
index 0000000000000000000000000000000000000000..5875098afa33a3ad923d10d66d6559537e587576
--- /dev/null
+++ b/ComfyUI/output/ComfyUI_00005_.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fe5b2c3fa1d440c1985eae77f399407977bc1b108c638ac590f56f3dda62ff9b
+size 5927526
diff --git a/ComfyUI/output/_output_images_will_be_put_here b/ComfyUI/output/_output_images_will_be_put_here
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/output/edgBodytape_00001.gif b/ComfyUI/output/edgBodytape_00001.gif
new file mode 100644
index 0000000000000000000000000000000000000000..5d243ef8281e421a62224f323f94c7440822e9f2
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00001.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ecd932db7c8413bfcef7a5d7bcaef737159cf37529b8799d43dbb3a14bc8b6ee
+size 10964491
diff --git a/ComfyUI/output/edgBodytape_00001.png b/ComfyUI/output/edgBodytape_00001.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c471ade91a47c44c65b6b2ad75d13ebe7f9781a
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00001.png differ
diff --git a/ComfyUI/output/edgBodytape_00002.gif b/ComfyUI/output/edgBodytape_00002.gif
new file mode 100644
index 0000000000000000000000000000000000000000..26864e36520eb27567c5b0684c37dbfa498a7a3a
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00002.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dec38ec86f46665e163332a1c1e95e612a76d6434dba632362acd98250f28630
+size 25550690
diff --git a/ComfyUI/output/edgBodytape_00002.png b/ComfyUI/output/edgBodytape_00002.png
new file mode 100644
index 0000000000000000000000000000000000000000..13d5cbd067ceb3a70fe6fc92552c3e88e520efc2
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00002.png differ
diff --git a/ComfyUI/output/edgBodytape_00003.gif b/ComfyUI/output/edgBodytape_00003.gif
new file mode 100644
index 0000000000000000000000000000000000000000..8972c623599a1da8a2e80e87126f9de2e662f93d
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00003.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6b52d7855a6375022dcff105da90894997fc7516e3bfdfcd814323e88225dbc4
+size 26551434
diff --git a/ComfyUI/output/edgBodytape_00003.png b/ComfyUI/output/edgBodytape_00003.png
new file mode 100644
index 0000000000000000000000000000000000000000..49fc08657a8b40dc47180ae0724853f7bd2f75da
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00003.png differ
diff --git a/ComfyUI/output/edgBodytape_00004.gif b/ComfyUI/output/edgBodytape_00004.gif
new file mode 100644
index 0000000000000000000000000000000000000000..29940420037dac1bb2ee74b82f95d9f033018883
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00004.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8405072df3faed212aa3ef12fe7d902e34a091b76c45bf59e81a2080e20f3e80
+size 26551434
diff --git a/ComfyUI/output/edgBodytape_00004.png b/ComfyUI/output/edgBodytape_00004.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2f8b13cc6c2a947aa14963c64658893d99ebb95
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00004.png differ
diff --git a/ComfyUI/output/edgBodytape_00005.gif b/ComfyUI/output/edgBodytape_00005.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fa1ad7f3edb5d23f955289a1b9564d0d4ab60b80
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00005.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:77a77eaa360e962ae5171c31cedfe147644a946961810fbe85392fca83fa2178
+size 26551232
diff --git a/ComfyUI/output/edgBodytape_00005.png b/ComfyUI/output/edgBodytape_00005.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d137ccabc359225443b1a2bad3eb4f1ecc0bc43
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00005.png differ
diff --git a/ComfyUI/output/edgBodytape_00006.gif b/ComfyUI/output/edgBodytape_00006.gif
new file mode 100644
index 0000000000000000000000000000000000000000..0f04121ef93d1f9aab87467fd8688df68a26c94e
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00006.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:256228f05e13294f1e03ae8f2d0d2aac0acb4e2c528766a323d27c2e7f2db5ce
+size 26551027
diff --git a/ComfyUI/output/edgBodytape_00006.png b/ComfyUI/output/edgBodytape_00006.png
new file mode 100644
index 0000000000000000000000000000000000000000..4cc374aaba02d94bd7b1ef318513367976e31bad
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00006.png differ
diff --git a/ComfyUI/output/edgBodytape_00007.gif b/ComfyUI/output/edgBodytape_00007.gif
new file mode 100644
index 0000000000000000000000000000000000000000..8d9491944e10f0e0adb73adc7fa80524a7e64947
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00007.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fd66167158fd21f43aa0056a081ea2a8d5f148dc20f7e82b369a8600316e7562
+size 10963065
diff --git a/ComfyUI/output/edgBodytape_00007.png b/ComfyUI/output/edgBodytape_00007.png
new file mode 100644
index 0000000000000000000000000000000000000000..53b98546d9ea2b485fbe723305719c6599f99ea8
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00007.png differ
diff --git a/ComfyUI/output/edgBodytape_00008.gif b/ComfyUI/output/edgBodytape_00008.gif
new file mode 100644
index 0000000000000000000000000000000000000000..be17bf13061baaa84afac00baecac0c82959c3c3
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00008.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:399e0f0c8ffab8443642b33ccd7662d962539649417fca8b22b0e5cc1f0aa6dc
+size 14428045
diff --git a/ComfyUI/output/edgBodytape_00008.png b/ComfyUI/output/edgBodytape_00008.png
new file mode 100644
index 0000000000000000000000000000000000000000..aeb39aa9684f29dc4c69a34baec09864f72858e9
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00008.png differ
diff --git a/ComfyUI/output/edgBodytape_00009.gif b/ComfyUI/output/edgBodytape_00009.gif
new file mode 100644
index 0000000000000000000000000000000000000000..4e91832c4f61c225f1d45b47c369993bd6fa098b
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00009.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0586757554aaf0aed87509369a973a5331d4080dcf6b1daf391eb5813133d037
+size 19131878
diff --git a/ComfyUI/output/edgBodytape_00009.png b/ComfyUI/output/edgBodytape_00009.png
new file mode 100644
index 0000000000000000000000000000000000000000..2d7755d0c530ec9e77371cf69938fe40f8ae94cc
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00009.png differ
diff --git a/ComfyUI/output/edgBodytape_00010.gif b/ComfyUI/output/edgBodytape_00010.gif
new file mode 100644
index 0000000000000000000000000000000000000000..488d2bc5b44c15a49c426e4cfbb7faec4d6ea026
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00010.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5fe7e929ca95a3902405c3626c014330ad5ae277624b4333bed5a08984a89b92
+size 18175191
diff --git a/ComfyUI/output/edgBodytape_00010.png b/ComfyUI/output/edgBodytape_00010.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ede4d445108e4298e78766e7f2a7bb8f97b520b
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00010.png differ
diff --git a/ComfyUI/output/edgBodytape_00011.gif b/ComfyUI/output/edgBodytape_00011.gif
new file mode 100644
index 0000000000000000000000000000000000000000..e87bf4978205a00ea3ed5e2e3ec1d20a06ee9c33
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00011.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:71347a45c690aacddc2acc33eac9efe5b2d875045b0421bcc144af6f2a623ceb
+size 18054211
diff --git a/ComfyUI/output/edgBodytape_00011.png b/ComfyUI/output/edgBodytape_00011.png
new file mode 100644
index 0000000000000000000000000000000000000000..256443bf4b5093f6265cefaf2c98e00008a6dcd5
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00011.png differ
diff --git a/ComfyUI/output/edgBodytape_00012.gif b/ComfyUI/output/edgBodytape_00012.gif
new file mode 100644
index 0000000000000000000000000000000000000000..6685c2cac854952aca50c014b677173c1ebbf398
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00012.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:15734b84a8a6fa3e912f237ee2ad9cfe3ad95042247a74044ade7dc8aa47ff47
+size 23536173
diff --git a/ComfyUI/output/edgBodytape_00012.png b/ComfyUI/output/edgBodytape_00012.png
new file mode 100644
index 0000000000000000000000000000000000000000..c85d24cba9a163660837a915957394c584e99285
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00012.png differ
diff --git a/ComfyUI/output/edgBodytape_00013.gif b/ComfyUI/output/edgBodytape_00013.gif
new file mode 100644
index 0000000000000000000000000000000000000000..f4023711f8b89207d5bed4ed1d1e91a2132065df
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00013.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0081fa3d7d0efe73c4877335146166ea924e7f7f1fb813ec8d30d63c566906a2
+size 19733277
diff --git a/ComfyUI/output/edgBodytape_00013.png b/ComfyUI/output/edgBodytape_00013.png
new file mode 100644
index 0000000000000000000000000000000000000000..832cab935765f427ce989c7e599aba34547b5075
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00013.png differ
diff --git a/ComfyUI/output/edgBodytape_00014.gif b/ComfyUI/output/edgBodytape_00014.gif
new file mode 100644
index 0000000000000000000000000000000000000000..432c4033bca5c7e6f7c2f7fd140920c857ba31c5
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00014.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a0ed4d7035c0060c92742be68fd9d348ea5950cdd4ad80ac8045a11ea3185328
+size 18653367
diff --git a/ComfyUI/output/edgBodytape_00014.png b/ComfyUI/output/edgBodytape_00014.png
new file mode 100644
index 0000000000000000000000000000000000000000..370edba42973ae7dc120f63884784312a5f579d3
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00014.png differ
diff --git a/ComfyUI/output/edgBodytape_00015.gif b/ComfyUI/output/edgBodytape_00015.gif
new file mode 100644
index 0000000000000000000000000000000000000000..e6aefe7ed1663adbeb272d009dd1cc63cf005e50
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00015.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:61c8e7c21c3d62e57aff2dc38aef583225b2735348aac5575ed07b03cd49d9e3
+size 18618148
diff --git a/ComfyUI/output/edgBodytape_00015.png b/ComfyUI/output/edgBodytape_00015.png
new file mode 100644
index 0000000000000000000000000000000000000000..075cdbc49eacab7b5807a0aad8391f81c0cdb875
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00015.png differ
diff --git a/ComfyUI/output/edgBodytape_00016.gif b/ComfyUI/output/edgBodytape_00016.gif
new file mode 100644
index 0000000000000000000000000000000000000000..e1e478edc9383e532c7eba939f63a867a78ac646
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00016.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:249ebe95f83dbf75db7deb357f7d72b5c0a67c58ffe9282e33a62fd080adf9a8
+size 17908747
diff --git a/ComfyUI/output/edgBodytape_00016.png b/ComfyUI/output/edgBodytape_00016.png
new file mode 100644
index 0000000000000000000000000000000000000000..a382fd2f07155821b4a648d02e66eda827797dbc
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00016.png differ
diff --git a/ComfyUI/output/edgBodytape_00017.gif b/ComfyUI/output/edgBodytape_00017.gif
new file mode 100644
index 0000000000000000000000000000000000000000..a93f2e756a061507089f061d663c37f6a80dee2f
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00017.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:69b9e30704ec4a48ca9bcd77091cf7e8ff8b3098a652c6a843d950146c915034
+size 19139383
diff --git a/ComfyUI/output/edgBodytape_00017.png b/ComfyUI/output/edgBodytape_00017.png
new file mode 100644
index 0000000000000000000000000000000000000000..086b9b7caaff7a684c8a244816ddc7129f708502
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00017.png differ
diff --git a/ComfyUI/output/edgBodytape_00018.gif b/ComfyUI/output/edgBodytape_00018.gif
new file mode 100644
index 0000000000000000000000000000000000000000..a17f9180a1cf6ba18bb955ee926a2c8a21f784e6
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00018.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee372d3cfcf05fb6474d10b92cdcd9e2d4576144d137f713c4d77be7065d0b9d
+size 18291689
diff --git a/ComfyUI/output/edgBodytape_00018.png b/ComfyUI/output/edgBodytape_00018.png
new file mode 100644
index 0000000000000000000000000000000000000000..7f4cb20289cbb12461b404eef2a1d6daa3213632
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00018.png differ
diff --git a/ComfyUI/output/edgBodytape_00019.gif b/ComfyUI/output/edgBodytape_00019.gif
new file mode 100644
index 0000000000000000000000000000000000000000..ec0723858394c9e49b2338f97721fa92857cf666
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00019.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cf6a89208b8d2896264520263422e7473bcea4fe8690b6891a3960cdbabdc8e6
+size 18716853
diff --git a/ComfyUI/output/edgBodytape_00019.png b/ComfyUI/output/edgBodytape_00019.png
new file mode 100644
index 0000000000000000000000000000000000000000..04db04f41a74f9cb21b078b0599a582ee95ae4d0
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00019.png differ
diff --git a/ComfyUI/output/edgBodytape_00020.gif b/ComfyUI/output/edgBodytape_00020.gif
new file mode 100644
index 0000000000000000000000000000000000000000..7bbf91182bab4b8050cc3ef38c00a38418df1879
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00020.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f8660f86a9301ecfc1da0c48e273879cea1520d3e398e57829b690f6316efdbc
+size 17086351
diff --git a/ComfyUI/output/edgBodytape_00020.png b/ComfyUI/output/edgBodytape_00020.png
new file mode 100644
index 0000000000000000000000000000000000000000..9a0e0143057bba84721d923634af8c79f134e55a
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00020.png differ
diff --git a/ComfyUI/output/edgBodytape_00021.gif b/ComfyUI/output/edgBodytape_00021.gif
new file mode 100644
index 0000000000000000000000000000000000000000..704670bc1c42ccdfc3269d9af3f9787a1eab183e
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00021.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:78d14a263350dd4d4992c29cc97d38f3cf6bd3a6fa4f2f7d90d53cf14e28910a
+size 17132674
diff --git a/ComfyUI/output/edgBodytape_00021.png b/ComfyUI/output/edgBodytape_00021.png
new file mode 100644
index 0000000000000000000000000000000000000000..67b1cc718497e36ea10cf0b70cf3fc274d01ef98
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00021.png differ
diff --git a/ComfyUI/output/edgBodytape_00022.gif b/ComfyUI/output/edgBodytape_00022.gif
new file mode 100644
index 0000000000000000000000000000000000000000..dac4595773e9baa31c4a0758e95f97ade9344b39
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00022.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:665b018cb8d3763600f15138a6b0f0c98500fdef462912f2bbd1fd73a9f16a80
+size 16361580
diff --git a/ComfyUI/output/edgBodytape_00022.png b/ComfyUI/output/edgBodytape_00022.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa87132436e1302d76f62fcd0c26a8447764015e
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00022.png differ
diff --git a/ComfyUI/output/edgBodytape_00023.gif b/ComfyUI/output/edgBodytape_00023.gif
new file mode 100644
index 0000000000000000000000000000000000000000..5140ade66f809a9e80c7bc4b3ba380805a2c5873
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00023.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0bc8a96effacd7d1d5c9ba684ac587ff3a8ef475ae2a277b0f68ddf0a371bc98
+size 13501932
diff --git a/ComfyUI/output/edgBodytape_00023.png b/ComfyUI/output/edgBodytape_00023.png
new file mode 100644
index 0000000000000000000000000000000000000000..b14776386e6f4943318a2cdc9a2291b5d7b4f466
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00023.png differ
diff --git a/ComfyUI/output/edgBodytape_00024.gif b/ComfyUI/output/edgBodytape_00024.gif
new file mode 100644
index 0000000000000000000000000000000000000000..3b4a947974ecdf962731d470e621a046eb7e54ee
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00024.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5d7ba8a864e966c2a75fe9842b230738c0474ac6ad7f2133927dc70da66fd8ba
+size 14703061
diff --git a/ComfyUI/output/edgBodytape_00024.png b/ComfyUI/output/edgBodytape_00024.png
new file mode 100644
index 0000000000000000000000000000000000000000..652d75b908f46e0eb4ce1865d17c488ac459739c
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00024.png differ
diff --git a/ComfyUI/output/edgBodytape_00025.gif b/ComfyUI/output/edgBodytape_00025.gif
new file mode 100644
index 0000000000000000000000000000000000000000..55bcee45e7d17ccbfb78a62f3dc0c7fcac443f1f
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00025.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e30747fe09123138e9ddddd969f8f204afa846eaffff13586e554094aeb30103
+size 16216825
diff --git a/ComfyUI/output/edgBodytape_00025.png b/ComfyUI/output/edgBodytape_00025.png
new file mode 100644
index 0000000000000000000000000000000000000000..8910ab476c70223ed04577c8ec766b78c6f3e941
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00025.png differ
diff --git a/ComfyUI/output/edgBodytape_00026.gif b/ComfyUI/output/edgBodytape_00026.gif
new file mode 100644
index 0000000000000000000000000000000000000000..a53882a4371c1a98846e57174d7ef315f322658e
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00026.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f816ab1755a8b01e884054de8ad6d9d2b13e78d2d03831ef12f69dacce8880a9
+size 15594482
diff --git a/ComfyUI/output/edgBodytape_00026.png b/ComfyUI/output/edgBodytape_00026.png
new file mode 100644
index 0000000000000000000000000000000000000000..553bd8332d203cb6c83db3262401be4af756a8ef
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00026.png differ
diff --git a/ComfyUI/output/edgBodytape_00027.gif b/ComfyUI/output/edgBodytape_00027.gif
new file mode 100644
index 0000000000000000000000000000000000000000..7631ceb918cd7022f194877c05ad24bd77858456
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00027.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:680db04597254fbb6c92643f4ec1540ae3a2bfea9a42254da94891e2a7987a0d
+size 11726434
diff --git a/ComfyUI/output/edgBodytape_00027.png b/ComfyUI/output/edgBodytape_00027.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0ca424b2cc1778081590ba308deef0ec650fc2e
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00027.png differ
diff --git a/ComfyUI/output/edgBodytape_00028.gif b/ComfyUI/output/edgBodytape_00028.gif
new file mode 100644
index 0000000000000000000000000000000000000000..e84ca12939dbe8733dc484e566291f08dd350e0b
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00028.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4bfd00d5d5a57843d6896d704c988fbdae8750ac0260a5f52f4d736518b836ef
+size 13247445
diff --git a/ComfyUI/output/edgBodytape_00028.png b/ComfyUI/output/edgBodytape_00028.png
new file mode 100644
index 0000000000000000000000000000000000000000..1551da10a231d2ac3ccca18edd45dbf4da6dc2c3
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00028.png differ
diff --git a/ComfyUI/output/edgBodytape_00029.gif b/ComfyUI/output/edgBodytape_00029.gif
new file mode 100644
index 0000000000000000000000000000000000000000..6e767370bb10b8c2815989795e59d44eb880f882
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00029.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d45539fd19cc61417bb110aeecb01d6398e8438489bf43e2e24e9c9e5ba3561
+size 13183062
diff --git a/ComfyUI/output/edgBodytape_00029.png b/ComfyUI/output/edgBodytape_00029.png
new file mode 100644
index 0000000000000000000000000000000000000000..0b8c6a22d1efa0fee450a1197f69ca42074b747a
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00029.png differ
diff --git a/ComfyUI/output/edgBodytape_00030.gif b/ComfyUI/output/edgBodytape_00030.gif
new file mode 100644
index 0000000000000000000000000000000000000000..ea28871bf21807f4ffce9e3967857761e2029c1e
--- /dev/null
+++ b/ComfyUI/output/edgBodytape_00030.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f51e2a7e8e94bfea8e944286cdce4baa5ae29ee253028bb807f22bee31de9f67
+size 9995437
diff --git a/ComfyUI/output/edgBodytape_00030.png b/ComfyUI/output/edgBodytape_00030.png
new file mode 100644
index 0000000000000000000000000000000000000000..5157c102993c1b9e81f4758209b21ddcf60da81f
Binary files /dev/null and b/ComfyUI/output/edgBodytape_00030.png differ
diff --git a/ComfyUI/pytest.ini b/ComfyUI/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..b5a68e0f12fe5bb51dc66310abe0237f5b0ff5c3
--- /dev/null
+++ b/ComfyUI/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+markers =
+ inference: mark as inference test (deselect with '-m "not inference"')
+testpaths = tests
+addopts = -s
\ No newline at end of file
diff --git a/ComfyUI/requirements.txt b/ComfyUI/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e804618e7156cf66e1c5437854358d0c652d9f14
--- /dev/null
+++ b/ComfyUI/requirements.txt
@@ -0,0 +1,12 @@
+torch
+torchsde
+torchvision
+einops
+transformers>=4.25.1
+safetensors>=0.3.0
+aiohttp
+pyyaml
+Pillow
+scipy
+tqdm
+psutil
diff --git a/ComfyUI/script_examples/basic_api_example.py b/ComfyUI/script_examples/basic_api_example.py
new file mode 100644
index 0000000000000000000000000000000000000000..242d3175f2eecceeae70ed54d7b0d4f02ae5cfb7
--- /dev/null
+++ b/ComfyUI/script_examples/basic_api_example.py
@@ -0,0 +1,120 @@
+import json
+from urllib import request, parse
+import random
+
+#This is the ComfyUI api prompt format.
+
+#If you want it for a specific workflow you can "enable dev mode options"
+#in the settings of the UI (gear beside the "Queue Size: ") this will enable
+#a button on the UI to save workflows in api format.
+
+#keep in mind ComfyUI is pre alpha software so this format will change a bit.
+
+#this is the one for the default workflow
+prompt_text = """
+{
+ "3": {
+ "class_type": "KSampler",
+ "inputs": {
+ "cfg": 8,
+ "denoise": 1,
+ "latent_image": [
+ "5",
+ 0
+ ],
+ "model": [
+ "4",
+ 0
+ ],
+ "negative": [
+ "7",
+ 0
+ ],
+ "positive": [
+ "6",
+ 0
+ ],
+ "sampler_name": "euler",
+ "scheduler": "normal",
+ "seed": 8566257,
+ "steps": 20
+ }
+ },
+ "4": {
+ "class_type": "CheckpointLoaderSimple",
+ "inputs": {
+ "ckpt_name": "v1-5-pruned-emaonly.ckpt"
+ }
+ },
+ "5": {
+ "class_type": "EmptyLatentImage",
+ "inputs": {
+ "batch_size": 1,
+ "height": 512,
+ "width": 512
+ }
+ },
+ "6": {
+ "class_type": "CLIPTextEncode",
+ "inputs": {
+ "clip": [
+ "4",
+ 1
+ ],
+ "text": "masterpiece best quality girl"
+ }
+ },
+ "7": {
+ "class_type": "CLIPTextEncode",
+ "inputs": {
+ "clip": [
+ "4",
+ 1
+ ],
+ "text": "bad hands"
+ }
+ },
+ "8": {
+ "class_type": "VAEDecode",
+ "inputs": {
+ "samples": [
+ "3",
+ 0
+ ],
+ "vae": [
+ "4",
+ 2
+ ]
+ }
+ },
+ "9": {
+ "class_type": "SaveImage",
+ "inputs": {
+ "filename_prefix": "ComfyUI",
+ "images": [
+ "8",
+ 0
+ ]
+ }
+ }
+}
+"""
+
+def queue_prompt(prompt):
+ p = {"prompt": prompt}
+ data = json.dumps(p).encode('utf-8')
+ req = request.Request("http://127.0.0.1:8188/prompt", data=data)
+ request.urlopen(req)
+
+
+prompt = json.loads(prompt_text)
+#set the text prompt for our positive CLIPTextEncode
+prompt["6"]["inputs"]["text"] = "masterpiece best quality man"
+
+#set the seed for our KSampler node
+prompt["3"]["inputs"]["seed"] = 5
+
+
+queue_prompt(prompt)
+
+
diff --git a/ComfyUI/script_examples/websockets_api_example.py b/ComfyUI/script_examples/websockets_api_example.py
new file mode 100644
index 0000000000000000000000000000000000000000..57a6cbd9bad181e547146e159a7969235dc9a945
--- /dev/null
+++ b/ComfyUI/script_examples/websockets_api_example.py
@@ -0,0 +1,164 @@
+#This is an example that uses the websockets api to know when a prompt execution is done
+#Once the prompt execution is done it downloads the images using the /history endpoint
+
+import websocket #NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
+import uuid
+import json
+import urllib.request
+import urllib.parse
+
+server_address = "127.0.0.1:8188"
+client_id = str(uuid.uuid4())
+
+def queue_prompt(prompt):
+ p = {"prompt": prompt, "client_id": client_id}
+ data = json.dumps(p).encode('utf-8')
+ req = urllib.request.Request("http://{}/prompt".format(server_address), data=data)
+ return json.loads(urllib.request.urlopen(req).read())
+
+def get_image(filename, subfolder, folder_type):
+ data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
+ url_values = urllib.parse.urlencode(data)
+ with urllib.request.urlopen("http://{}/view?{}".format(server_address, url_values)) as response:
+ return response.read()
+
+def get_history(prompt_id):
+ with urllib.request.urlopen("http://{}/history/{}".format(server_address, prompt_id)) as response:
+ return json.loads(response.read())
+
+def get_images(ws, prompt):
+ prompt_id = queue_prompt(prompt)['prompt_id']
+ output_images = {}
+ while True:
+ out = ws.recv()
+ if isinstance(out, str):
+ message = json.loads(out)
+ if message['type'] == 'executing':
+ data = message['data']
+ if data['node'] is None and data['prompt_id'] == prompt_id:
+ break #Execution is done
+ else:
+ continue #previews are binary data
+
+ history = get_history(prompt_id)[prompt_id]
+ for o in history['outputs']:
+ for node_id in history['outputs']:
+ node_output = history['outputs'][node_id]
+ if 'images' in node_output:
+ images_output = []
+ for image in node_output['images']:
+ image_data = get_image(image['filename'], image['subfolder'], image['type'])
+ images_output.append(image_data)
+ output_images[node_id] = images_output
+
+ return output_images
+
+prompt_text = """
+{
+ "3": {
+ "class_type": "KSampler",
+ "inputs": {
+ "cfg": 8,
+ "denoise": 1,
+ "latent_image": [
+ "5",
+ 0
+ ],
+ "model": [
+ "4",
+ 0
+ ],
+ "negative": [
+ "7",
+ 0
+ ],
+ "positive": [
+ "6",
+ 0
+ ],
+ "sampler_name": "euler",
+ "scheduler": "normal",
+ "seed": 8566257,
+ "steps": 20
+ }
+ },
+ "4": {
+ "class_type": "CheckpointLoaderSimple",
+ "inputs": {
+ "ckpt_name": "v1-5-pruned-emaonly.ckpt"
+ }
+ },
+ "5": {
+ "class_type": "EmptyLatentImage",
+ "inputs": {
+ "batch_size": 1,
+ "height": 512,
+ "width": 512
+ }
+ },
+ "6": {
+ "class_type": "CLIPTextEncode",
+ "inputs": {
+ "clip": [
+ "4",
+ 1
+ ],
+ "text": "masterpiece best quality girl"
+ }
+ },
+ "7": {
+ "class_type": "CLIPTextEncode",
+ "inputs": {
+ "clip": [
+ "4",
+ 1
+ ],
+ "text": "bad hands"
+ }
+ },
+ "8": {
+ "class_type": "VAEDecode",
+ "inputs": {
+ "samples": [
+ "3",
+ 0
+ ],
+ "vae": [
+ "4",
+ 2
+ ]
+ }
+ },
+ "9": {
+ "class_type": "SaveImage",
+ "inputs": {
+ "filename_prefix": "ComfyUI",
+ "images": [
+ "8",
+ 0
+ ]
+ }
+ }
+}
+"""
+
+prompt = json.loads(prompt_text)
+#set the text prompt for our positive CLIPTextEncode
+prompt["6"]["inputs"]["text"] = "masterpiece best quality man"
+
+#set the seed for our KSampler node
+prompt["3"]["inputs"]["seed"] = 5
+
+ws = websocket.WebSocket()
+ws.connect("ws://{}/ws?clientId={}".format(server_address, client_id))
+images = get_images(ws, prompt)
+
+#Commented out code to display the output images:
+
+# for node_id in images:
+# for image_data in images[node_id]:
+# from PIL import Image
+# import io
+# image = Image.open(io.BytesIO(image_data))
+# image.show()
+
diff --git a/ComfyUI/server.py b/ComfyUI/server.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b1e3269d7fc8cc6c2ba4781e23d69414c0b84fa
--- /dev/null
+++ b/ComfyUI/server.py
@@ -0,0 +1,638 @@
+import os
+import sys
+import asyncio
+import traceback
+
+import nodes
+import folder_paths
+import execution
+import uuid
+import urllib
+import json
+import glob
+import struct
+from PIL import Image, ImageOps
+from PIL.PngImagePlugin import PngInfo
+from io import BytesIO
+
+try:
+ import aiohttp
+ from aiohttp import web
+except ImportError:
+ print("Module 'aiohttp' not installed. Please install it via:")
+ print("pip install aiohttp")
+ print("or")
+ print("pip install -r requirements.txt")
+ sys.exit()
+
+import mimetypes
+from comfy.cli_args import args
+import comfy.utils
+import comfy.model_management
+
+
+class BinaryEventTypes:
+ PREVIEW_IMAGE = 1
+ UNENCODED_PREVIEW_IMAGE = 2
+
+async def send_socket_catch_exception(function, message):
+ try:
+ await function(message)
+ except (aiohttp.ClientError, aiohttp.ClientPayloadError, ConnectionResetError) as err:
+ print("send error:", err)
+
+@web.middleware
+async def cache_control(request: web.Request, handler):
+ response: web.Response = await handler(request)
+ if request.path.endswith('.js') or request.path.endswith('.css'):
+ response.headers.setdefault('Cache-Control', 'no-cache')
+ return response
+
+def create_cors_middleware(allowed_origin: str):
+ @web.middleware
+ async def cors_middleware(request: web.Request, handler):
+ if request.method == "OPTIONS":
+ # Pre-flight request. Reply successfully:
+ response = web.Response()
+ else:
+ response = await handler(request)
+
+ response.headers['Access-Control-Allow-Origin'] = allowed_origin
+ response.headers['Access-Control-Allow-Methods'] = 'POST, GET, DELETE, PUT, OPTIONS'
+ response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
+ response.headers['Access-Control-Allow-Credentials'] = 'true'
+ return response
+
+ return cors_middleware
+
+class PromptServer():
+ def __init__(self, loop):
+ PromptServer.instance = self
+
+ mimetypes.init()
+ mimetypes.types_map['.js'] = 'application/javascript; charset=utf-8'
+
+ self.supports = ["custom_nodes_from_web"]
+ self.prompt_queue = None
+ self.loop = loop
+ self.messages = asyncio.Queue()
+ self.number = 0
+
+ middlewares = [cache_control]
+ if args.enable_cors_header:
+ middlewares.append(create_cors_middleware(args.enable_cors_header))
+
+ max_upload_size = round(args.max_upload_size * 1024 * 1024)
+ self.app = web.Application(client_max_size=max_upload_size, middlewares=middlewares)
+ self.sockets = dict()
+ self.web_root = os.path.join(os.path.dirname(
+ os.path.realpath(__file__)), "web")
+ routes = web.RouteTableDef()
+ self.routes = routes
+ self.last_node_id = None
+ self.client_id = None
+
+ self.on_prompt_handlers = []
+
+ @routes.get('/ws')
+ async def websocket_handler(request):
+ ws = web.WebSocketResponse()
+ await ws.prepare(request)
+ sid = request.rel_url.query.get('clientId', '')
+ if sid:
+ # Reusing existing session, remove old
+ self.sockets.pop(sid, None)
+ else:
+ sid = uuid.uuid4().hex
+
+ self.sockets[sid] = ws
+
+ try:
+ # Send initial state to the new client
+ await self.send("status", { "status": self.get_queue_info(), 'sid': sid }, sid)
+ # On reconnect if we are the currently executing client send the current node
+ if self.client_id == sid and self.last_node_id is not None:
+ await self.send("executing", { "node": self.last_node_id }, sid)
+
+ async for msg in ws:
+ if msg.type == aiohttp.WSMsgType.ERROR:
+ print('ws connection closed with exception %s' % ws.exception())
+ finally:
+ self.sockets.pop(sid, None)
+ return ws
+
+ @routes.get("/")
+ async def get_root(request):
+ return web.FileResponse(os.path.join(self.web_root, "index.html"))
+
+ @routes.get("/embeddings")
+ def get_embeddings(self):
+ embeddings = folder_paths.get_filename_list("embeddings")
+ return web.json_response(list(map(lambda a: os.path.splitext(a)[0], embeddings)))
+
+ @routes.get("/extensions")
+ async def get_extensions(request):
+ files = glob.glob(os.path.join(
+ glob.escape(self.web_root), 'extensions/**/*.js'), recursive=True)
+
+ extensions = list(map(lambda f: "/" + os.path.relpath(f, self.web_root).replace("\\", "/"), files))
+
+ for name, dir in nodes.EXTENSION_WEB_DIRS.items():
+ files = glob.glob(os.path.join(glob.escape(dir), '**/*.js'), recursive=True)
+ extensions.extend(list(map(lambda f: "/extensions/" + urllib.parse.quote(
+ name) + "/" + os.path.relpath(f, dir).replace("\\", "/"), files)))
+
+ return web.json_response(extensions)
+
+ def get_dir_by_type(dir_type):
+ if dir_type is None:
+ dir_type = "input"
+
+ if dir_type == "input":
+ type_dir = folder_paths.get_input_directory()
+ elif dir_type == "temp":
+ type_dir = folder_paths.get_temp_directory()
+ elif dir_type == "output":
+ type_dir = folder_paths.get_output_directory()
+
+ return type_dir, dir_type
+
+ def image_upload(post, image_save_function=None):
+ image = post.get("image")
+ overwrite = post.get("overwrite")
+
+ image_upload_type = post.get("type")
+ upload_dir, image_upload_type = get_dir_by_type(image_upload_type)
+
+ if image and image.file:
+ filename = image.filename
+ if not filename:
+ return web.Response(status=400)
+
+ subfolder = post.get("subfolder", "")
+ full_output_folder = os.path.join(upload_dir, os.path.normpath(subfolder))
+ filepath = os.path.abspath(os.path.join(full_output_folder, filename))
+
+ if os.path.commonpath((upload_dir, filepath)) != upload_dir:
+ return web.Response(status=400)
+
+ if not os.path.exists(full_output_folder):
+ os.makedirs(full_output_folder)
+
+ split = os.path.splitext(filename)
+
+ if overwrite is not None and (overwrite == "true" or overwrite == "1"):
+ pass
+ else:
+ i = 1
+ while os.path.exists(filepath):
+ filename = f"{split[0]} ({i}){split[1]}"
+ filepath = os.path.join(full_output_folder, filename)
+ i += 1
+
+ if image_save_function is not None:
+ image_save_function(image, post, filepath)
+ else:
+ with open(filepath, "wb") as f:
+ f.write(image.file.read())
+
+ return web.json_response({"name" : filename, "subfolder": subfolder, "type": image_upload_type})
+ else:
+ return web.Response(status=400)
+
+ @routes.post("/upload/image")
+ async def upload_image(request):
+ post = await request.post()
+ return image_upload(post)
+
+
+ @routes.post("/upload/mask")
+ async def upload_mask(request):
+ post = await request.post()
+
+ def image_save_function(image, post, filepath):
+ original_ref = json.loads(post.get("original_ref"))
+ filename, output_dir = folder_paths.annotated_filepath(original_ref['filename'])
+
+ # validation for security: prevent accessing arbitrary path
+ if filename[0] == '/' or '..' in filename:
+ return web.Response(status=400)
+
+ if output_dir is None:
+ type = original_ref.get("type", "output")
+ output_dir = folder_paths.get_directory_by_type(type)
+
+ if output_dir is None:
+ return web.Response(status=400)
+
+ if original_ref.get("subfolder", "") != "":
+ full_output_dir = os.path.join(output_dir, original_ref["subfolder"])
+ if os.path.commonpath((os.path.abspath(full_output_dir), output_dir)) != output_dir:
+ return web.Response(status=403)
+ output_dir = full_output_dir
+
+ file = os.path.join(output_dir, filename)
+
+ if os.path.isfile(file):
+ with Image.open(file) as original_pil:
+ metadata = PngInfo()
+ if hasattr(original_pil,'text'):
+ for key in original_pil.text:
+ metadata.add_text(key, original_pil.text[key])
+ original_pil = original_pil.convert('RGBA')
+ mask_pil = Image.open(image.file).convert('RGBA')
+
+ # alpha copy
+ new_alpha = mask_pil.getchannel('A')
+ original_pil.putalpha(new_alpha)
+ original_pil.save(filepath, compress_level=4, pnginfo=metadata)
+
+ return image_upload(post, image_save_function)
+
+ @routes.get("/view")
+ async def view_image(request):
+ if "filename" in request.rel_url.query:
+ filename = request.rel_url.query["filename"]
+ filename,output_dir = folder_paths.annotated_filepath(filename)
+
+ # validation for security: prevent accessing arbitrary path
+ if filename[0] == '/' or '..' in filename:
+ return web.Response(status=400)
+
+ if output_dir is None:
+ type = request.rel_url.query.get("type", "output")
+ output_dir = folder_paths.get_directory_by_type(type)
+
+ if output_dir is None:
+ return web.Response(status=400)
+
+ if "subfolder" in request.rel_url.query:
+ full_output_dir = os.path.join(output_dir, request.rel_url.query["subfolder"])
+ if os.path.commonpath((os.path.abspath(full_output_dir), output_dir)) != output_dir:
+ return web.Response(status=403)
+ output_dir = full_output_dir
+
+ filename = os.path.basename(filename)
+ file = os.path.join(output_dir, filename)
+
+ if os.path.isfile(file):
+ if 'preview' in request.rel_url.query:
+ with Image.open(file) as img:
+ preview_info = request.rel_url.query['preview'].split(';')
+ image_format = preview_info[0]
+ if image_format not in ['webp', 'jpeg'] or 'a' in request.rel_url.query.get('channel', ''):
+ image_format = 'webp'
+
+ quality = 90
+ if preview_info[-1].isdigit():
+ quality = int(preview_info[-1])
+
+ buffer = BytesIO()
+ if image_format in ['jpeg'] or request.rel_url.query.get('channel', '') == 'rgb':
+ img = img.convert("RGB")
+ img.save(buffer, format=image_format, quality=quality)
+ buffer.seek(0)
+
+ return web.Response(body=buffer.read(), content_type=f'image/{image_format}',
+ headers={"Content-Disposition": f"filename=\"{filename}\""})
+
+ if 'channel' not in request.rel_url.query:
+ channel = 'rgba'
+ else:
+ channel = request.rel_url.query["channel"]
+
+ if channel == 'rgb':
+ with Image.open(file) as img:
+ if img.mode == "RGBA":
+ r, g, b, a = img.split()
+ new_img = Image.merge('RGB', (r, g, b))
+ else:
+ new_img = img.convert("RGB")
+
+ buffer = BytesIO()
+ new_img.save(buffer, format='PNG')
+ buffer.seek(0)
+
+ return web.Response(body=buffer.read(), content_type='image/png',
+ headers={"Content-Disposition": f"filename=\"{filename}\""})
+
+ elif channel == 'a':
+ with Image.open(file) as img:
+ if img.mode == "RGBA":
+ _, _, _, a = img.split()
+ else:
+ a = Image.new('L', img.size, 255)
+
+ # alpha img
+ alpha_img = Image.new('RGBA', img.size)
+ alpha_img.putalpha(a)
+ alpha_buffer = BytesIO()
+ alpha_img.save(alpha_buffer, format='PNG')
+ alpha_buffer.seek(0)
+
+ return web.Response(body=alpha_buffer.read(), content_type='image/png',
+ headers={"Content-Disposition": f"filename=\"{filename}\""})
+ else:
+ return web.FileResponse(file, headers={"Content-Disposition": f"filename=\"{filename}\""})
+
+ return web.Response(status=404)
+
+ @routes.get("/view_metadata/{folder_name}")
+ async def view_metadata(request):
+ folder_name = request.match_info.get("folder_name", None)
+ if folder_name is None:
+ return web.Response(status=404)
+ if not "filename" in request.rel_url.query:
+ return web.Response(status=404)
+
+ filename = request.rel_url.query["filename"]
+ if not filename.endswith(".safetensors"):
+ return web.Response(status=404)
+
+ safetensors_path = folder_paths.get_full_path(folder_name, filename)
+ if safetensors_path is None:
+ return web.Response(status=404)
+ out = comfy.utils.safetensors_header(safetensors_path, max_size=1024*1024)
+ if out is None:
+ return web.Response(status=404)
+ dt = json.loads(out)
+ if not "__metadata__" in dt:
+ return web.Response(status=404)
+ return web.json_response(dt["__metadata__"])
+
+ @routes.get("/system_stats")
+ async def get_queue(request):
+ device = comfy.model_management.get_torch_device()
+ device_name = comfy.model_management.get_torch_device_name(device)
+ vram_total, torch_vram_total = comfy.model_management.get_total_memory(device, torch_total_too=True)
+ vram_free, torch_vram_free = comfy.model_management.get_free_memory(device, torch_free_too=True)
+ system_stats = {
+ "system": {
+ "os": os.name,
+ "python_version": sys.version,
+ "embedded_python": os.path.split(os.path.split(sys.executable)[0])[1] == "python_embeded"
+ },
+ "devices": [
+ {
+ "name": device_name,
+ "type": device.type,
+ "index": device.index,
+ "vram_total": vram_total,
+ "vram_free": vram_free,
+ "torch_vram_total": torch_vram_total,
+ "torch_vram_free": torch_vram_free,
+ }
+ ]
+ }
+ return web.json_response(system_stats)
+
+ @routes.get("/prompt")
+ async def get_prompt(request):
+ return web.json_response(self.get_queue_info())
+
+ def node_info(node_class):
+ obj_class = nodes.NODE_CLASS_MAPPINGS[node_class]
+ info = {}
+ info['input'] = obj_class.INPUT_TYPES()
+ info['output'] = obj_class.RETURN_TYPES
+ info['output_is_list'] = obj_class.OUTPUT_IS_LIST if hasattr(obj_class, 'OUTPUT_IS_LIST') else [False] * len(obj_class.RETURN_TYPES)
+ info['output_name'] = obj_class.RETURN_NAMES if hasattr(obj_class, 'RETURN_NAMES') else info['output']
+ info['name'] = node_class
+ info['display_name'] = nodes.NODE_DISPLAY_NAME_MAPPINGS[node_class] if node_class in nodes.NODE_DISPLAY_NAME_MAPPINGS.keys() else node_class
+ info['description'] = obj_class.DESCRIPTION if hasattr(obj_class,'DESCRIPTION') else ''
+ info['category'] = 'sd'
+ if hasattr(obj_class, 'OUTPUT_NODE') and obj_class.OUTPUT_NODE == True:
+ info['output_node'] = True
+ else:
+ info['output_node'] = False
+
+ if hasattr(obj_class, 'CATEGORY'):
+ info['category'] = obj_class.CATEGORY
+ return info
+
+ @routes.get("/object_info")
+ async def get_object_info(request):
+ out = {}
+ for x in nodes.NODE_CLASS_MAPPINGS:
+ try:
+ out[x] = node_info(x)
+ except Exception as e:
+ print(f"[ERROR] An error occurred while retrieving information for the '{x}' node.", file=sys.stderr)
+ traceback.print_exc()
+ return web.json_response(out)
+
+ @routes.get("/object_info/{node_class}")
+ async def get_object_info_node(request):
+ node_class = request.match_info.get("node_class", None)
+ out = {}
+ if (node_class is not None) and (node_class in nodes.NODE_CLASS_MAPPINGS):
+ out[node_class] = node_info(node_class)
+ return web.json_response(out)
+
+ @routes.get("/history")
+ async def get_history(request):
+ max_items = request.rel_url.query.get("max_items", None)
+ if max_items is not None:
+ max_items = int(max_items)
+ return web.json_response(self.prompt_queue.get_history(max_items=max_items))
+
+ @routes.get("/history/{prompt_id}")
+ async def get_history(request):
+ prompt_id = request.match_info.get("prompt_id", None)
+ return web.json_response(self.prompt_queue.get_history(prompt_id=prompt_id))
+
+ @routes.get("/queue")
+ async def get_queue(request):
+ queue_info = {}
+ current_queue = self.prompt_queue.get_current_queue()
+ queue_info['queue_running'] = current_queue[0]
+ queue_info['queue_pending'] = current_queue[1]
+ return web.json_response(queue_info)
+
+ @routes.post("/prompt")
+ async def post_prompt(request):
+ print("got prompt")
+ resp_code = 200
+ out_string = ""
+ json_data = await request.json()
+ json_data = self.trigger_on_prompt(json_data)
+
+ if "number" in json_data:
+ number = float(json_data['number'])
+ else:
+ number = self.number
+ if "front" in json_data:
+ if json_data['front']:
+ number = -number
+
+ self.number += 1
+
+ if "prompt" in json_data:
+ prompt = json_data["prompt"]
+ valid = execution.validate_prompt(prompt)
+ extra_data = {}
+ if "extra_data" in json_data:
+ extra_data = json_data["extra_data"]
+
+ if "client_id" in json_data:
+ extra_data["client_id"] = json_data["client_id"]
+ if valid[0]:
+ prompt_id = str(uuid.uuid4())
+ outputs_to_execute = valid[2]
+ self.prompt_queue.put((number, prompt_id, prompt, extra_data, outputs_to_execute))
+ response = {"prompt_id": prompt_id, "number": number, "node_errors": valid[3]}
+ return web.json_response(response)
+ else:
+ print("invalid prompt:", valid[1])
+ return web.json_response({"error": valid[1], "node_errors": valid[3]}, status=400)
+ else:
+ return web.json_response({"error": "no prompt", "node_errors": []}, status=400)
+
+ @routes.post("/queue")
+ async def post_queue(request):
+ json_data = await request.json()
+ if "clear" in json_data:
+ if json_data["clear"]:
+ self.prompt_queue.wipe_queue()
+ if "delete" in json_data:
+ to_delete = json_data['delete']
+ for id_to_delete in to_delete:
+ delete_func = lambda a: a[1] == id_to_delete
+ self.prompt_queue.delete_queue_item(delete_func)
+
+ return web.Response(status=200)
+
+ @routes.post("/interrupt")
+ async def post_interrupt(request):
+ nodes.interrupt_processing()
+ return web.Response(status=200)
+
+ @routes.post("/history")
+ async def post_history(request):
+ json_data = await request.json()
+ if "clear" in json_data:
+ if json_data["clear"]:
+ self.prompt_queue.wipe_history()
+ if "delete" in json_data:
+ to_delete = json_data['delete']
+ for id_to_delete in to_delete:
+ self.prompt_queue.delete_history_item(id_to_delete)
+
+ return web.Response(status=200)
+
+ def add_routes(self):
+ self.app.add_routes(self.routes)
+
+ for name, dir in nodes.EXTENSION_WEB_DIRS.items():
+ self.app.add_routes([
+ web.static('/extensions/' + urllib.parse.quote(name), dir, follow_symlinks=True),
+ ])
+
+ self.app.add_routes([
+ web.static('/', self.web_root, follow_symlinks=True),
+ ])
+
+ def get_queue_info(self):
+ prompt_info = {}
+ exec_info = {}
+ exec_info['queue_remaining'] = self.prompt_queue.get_tasks_remaining()
+ prompt_info['exec_info'] = exec_info
+ return prompt_info
+
+ async def send(self, event, data, sid=None):
+ if event == BinaryEventTypes.UNENCODED_PREVIEW_IMAGE:
+ await self.send_image(data, sid=sid)
+ elif isinstance(data, (bytes, bytearray)):
+ await self.send_bytes(event, data, sid)
+ else:
+ await self.send_json(event, data, sid)
+
+ def encode_bytes(self, event, data):
+ if not isinstance(event, int):
+ raise RuntimeError(f"Binary event types must be integers, got {event}")
+
+ packed = struct.pack(">I", event)
+ message = bytearray(packed)
+ message.extend(data)
+ return message
+
+ async def send_image(self, image_data, sid=None):
+ image_type = image_data[0]
+ image = image_data[1]
+ max_size = image_data[2]
+ if max_size is not None:
+ if hasattr(Image, 'Resampling'):
+ resampling = Image.Resampling.BILINEAR
+ else:
+ resampling = Image.ANTIALIAS
+
+ image = ImageOps.contain(image, (max_size, max_size), resampling)
+ type_num = 1
+ if image_type == "JPEG":
+ type_num = 1
+ elif image_type == "PNG":
+ type_num = 2
+
+ bytesIO = BytesIO()
+ header = struct.pack(">I", type_num)
+ bytesIO.write(header)
+ image.save(bytesIO, format=image_type, quality=95, compress_level=1)
+ preview_bytes = bytesIO.getvalue()
+ await self.send_bytes(BinaryEventTypes.PREVIEW_IMAGE, preview_bytes, sid=sid)
+
+ async def send_bytes(self, event, data, sid=None):
+ message = self.encode_bytes(event, data)
+
+ if sid is None:
+ for ws in self.sockets.values():
+ await send_socket_catch_exception(ws.send_bytes, message)
+ elif sid in self.sockets:
+ await send_socket_catch_exception(self.sockets[sid].send_bytes, message)
+
+ async def send_json(self, event, data, sid=None):
+ message = {"type": event, "data": data}
+
+ if sid is None:
+ for ws in self.sockets.values():
+ await send_socket_catch_exception(ws.send_json, message)
+ elif sid in self.sockets:
+ await send_socket_catch_exception(self.sockets[sid].send_json, message)
+
+ def send_sync(self, event, data, sid=None):
+ self.loop.call_soon_threadsafe(
+ self.messages.put_nowait, (event, data, sid))
+
+ def queue_updated(self):
+ self.send_sync("status", { "status": self.get_queue_info() })
+
+ async def publish_loop(self):
+ while True:
+ msg = await self.messages.get()
+ await self.send(*msg)
+
+ async def start(self, address, port, verbose=True, call_on_start=None):
+ runner = web.AppRunner(self.app, access_log=None)
+ await runner.setup()
+ site = web.TCPSite(runner, address, port)
+ await site.start()
+
+ if address == '':
+ address = '0.0.0.0'
+ if verbose:
+ print("Starting server\n")
+ print("To see the GUI go to: http://{}:{}".format(address, port))
+ if call_on_start is not None:
+ call_on_start(address, port)
+
+ def add_on_prompt_handler(self, handler):
+ self.on_prompt_handlers.append(handler)
+
+ def trigger_on_prompt(self, json_data):
+ for handler in self.on_prompt_handlers:
+ try:
+ json_data = handler(json_data)
+ except Exception as e:
+ print(f"[ERROR] An error occurred during the on_prompt_handler processing")
+ traceback.print_exc()
+
+ return json_data
diff --git a/ComfyUI/tests-ui/.gitignore b/ComfyUI/tests-ui/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..b512c09d476623ff4bf8d0d63c29b784925dbdf8
--- /dev/null
+++ b/ComfyUI/tests-ui/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/ComfyUI/tests-ui/afterSetup.js b/ComfyUI/tests-ui/afterSetup.js
new file mode 100644
index 0000000000000000000000000000000000000000..983f3af643cda02096668e4ac092c3f252214ab8
--- /dev/null
+++ b/ComfyUI/tests-ui/afterSetup.js
@@ -0,0 +1,9 @@
+const { start } = require("./utils");
+const lg = require("./utils/litegraph");
+
+// Load things once per test file before to ensure its all warmed up for the tests
+beforeAll(async () => {
+ lg.setup(global);
+ await start({ resetEnv: true });
+ lg.teardown(global);
+});
diff --git a/ComfyUI/tests-ui/babel.config.json b/ComfyUI/tests-ui/babel.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..526ddfd8df1146d2750d61dc0793b366c7e81e82
--- /dev/null
+++ b/ComfyUI/tests-ui/babel.config.json
@@ -0,0 +1,3 @@
+{
+ "presets": ["@babel/preset-env"]
+}
diff --git a/ComfyUI/tests-ui/globalSetup.js b/ComfyUI/tests-ui/globalSetup.js
new file mode 100644
index 0000000000000000000000000000000000000000..b9d97f58a96f90b3b7f7fc66b15183edcc0837ab
--- /dev/null
+++ b/ComfyUI/tests-ui/globalSetup.js
@@ -0,0 +1,14 @@
+module.exports = async function () {
+ global.ResizeObserver = class ResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ };
+
+ const { nop } = require("./utils/nopProxy");
+ global.enableWebGLCanvas = nop;
+
+ HTMLCanvasElement.prototype.getContext = nop;
+
+ localStorage["Comfy.Settings.Comfy.Logging.Enabled"] = "false";
+};
diff --git a/ComfyUI/tests-ui/jest.config.js b/ComfyUI/tests-ui/jest.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..86fff50574b099dca3365e19e4e5bb48454ed80d
--- /dev/null
+++ b/ComfyUI/tests-ui/jest.config.js
@@ -0,0 +1,11 @@
+/** @type {import('jest').Config} */
+const config = {
+ testEnvironment: "jsdom",
+ setupFiles: ["./globalSetup.js"],
+ setupFilesAfterEnv: ["./afterSetup.js"],
+ clearMocks: true,
+ resetModules: true,
+ testTimeout: 10000
+};
+
+module.exports = config;
diff --git a/ComfyUI/tests-ui/package-lock.json b/ComfyUI/tests-ui/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..35911cd7ffde34152e24f80cde3e22e694d1adf2
--- /dev/null
+++ b/ComfyUI/tests-ui/package-lock.json
@@ -0,0 +1,5566 @@
+{
+ "name": "comfui-tests",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "comfui-tests",
+ "version": "1.0.0",
+ "license": "GPL-3.0",
+ "devDependencies": {
+ "@babel/preset-env": "^7.22.20",
+ "@types/jest": "^29.5.5",
+ "jest": "^29.7.0",
+ "jest-environment-jsdom": "^29.7.0"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
+ "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.22.13",
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz",
+ "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz",
+ "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==",
+ "dev": true,
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-compilation-targets": "^7.22.15",
+ "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helpers": "^7.23.0",
+ "@babel/parser": "^7.23.0",
+ "@babel/template": "^7.22.15",
+ "@babel/traverse": "^7.23.0",
+ "@babel/types": "^7.23.0",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.23.0",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "@jridgewell/trace-mapping": "^0.3.17",
+ "jsesc": "^2.5.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
+ "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz",
+ "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
+ "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.22.9",
+ "@babel/helper-validator-option": "^7.22.15",
+ "browserslist": "^4.21.9",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz",
+ "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-member-expression-to-functions": "^7.22.15",
+ "@babel/helper-optimise-call-expression": "^7.22.5",
+ "@babel/helper-replace-supers": "^7.22.9",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz",
+ "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "regexpu-core": "^5.3.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz",
+ "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.22.6",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "debug": "^4.1.1",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.14.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-environment-visitor": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-function-name": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-hoist-variables": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz",
+ "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.23.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
+ "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz",
+ "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-module-imports": "^7.22.15",
+ "@babel/helper-simple-access": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/helper-validator-identifier": "^7.22.20"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
+ "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
+ "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz",
+ "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-wrap-function": "^7.22.20"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz",
+ "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-member-expression-to-functions": "^7.22.15",
+ "@babel/helper-optimise-call-expression": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-simple-access": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
+ "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
+ "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
+ "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz",
+ "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.22.19"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.23.1",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz",
+ "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.22.15",
+ "@babel/traverse": "^7.23.0",
+ "@babel/types": "^7.23.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
+ "dev": true,
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz",
+ "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz",
+ "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+ "@babel/plugin-transform-optional-chaining": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.13.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-bigint": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
+ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-properties": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+ "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-static-block": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
+ "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-dynamic-import": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
+ "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-export-namespace-from": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz",
+ "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz",
+ "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz",
+ "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz",
+ "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
+ "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-private-property-in-object": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
+ "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
+ "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz",
+ "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
+ "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz",
+ "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-generator-functions": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz",
+ "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-remap-async-to-generator": "^7.22.9",
+ "@babel/plugin-syntax-async-generators": "^7.8.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz",
+ "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-remap-async-to-generator": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz",
+ "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz",
+ "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-properties": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz",
+ "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-static-block": {
+ "version": "7.22.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz",
+ "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.22.11",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz",
+ "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-compilation-targets": "^7.22.15",
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-optimise-call-expression": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-replace-supers": "^7.22.9",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz",
+ "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/template": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz",
+ "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz",
+ "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz",
+ "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dynamic-import": {
+ "version": "7.22.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz",
+ "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz",
+ "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-export-namespace-from": {
+ "version": "7.22.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz",
+ "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-export-namespace-from": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-for-of": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz",
+ "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz",
+ "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.22.5",
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-json-strings": {
+ "version": "7.22.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz",
+ "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-json-strings": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz",
+ "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-logical-assignment-operators": {
+ "version": "7.22.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz",
+ "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz",
+ "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz",
+ "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz",
+ "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-simple-access": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz",
+ "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz",
+ "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz",
+ "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-new-target": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz",
+ "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
+ "version": "7.22.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz",
+ "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-numeric-separator": {
+ "version": "7.22.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz",
+ "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-rest-spread": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz",
+ "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.22.9",
+ "@babel/helper-compilation-targets": "^7.22.15",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-transform-parameters": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-super": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz",
+ "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-replace-supers": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-catch-binding": {
+ "version": "7.22.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz",
+ "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-chaining": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz",
+ "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz",
+ "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-methods": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz",
+ "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-property-in-object": {
+ "version": "7.22.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz",
+ "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-create-class-features-plugin": "^7.22.11",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-property-literals": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz",
+ "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.22.10",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz",
+ "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "regenerator-transform": "^0.15.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-reserved-words": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz",
+ "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-shorthand-properties": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz",
+ "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-spread": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz",
+ "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-sticky-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz",
+ "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-template-literals": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz",
+ "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typeof-symbol": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz",
+ "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-escapes": {
+ "version": "7.22.10",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz",
+ "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-property-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz",
+ "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz",
+ "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-sets-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz",
+ "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/preset-env": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz",
+ "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.22.20",
+ "@babel/helper-compilation-targets": "^7.22.15",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-validator-option": "^7.22.15",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
+ "@babel/plugin-syntax-export-namespace-from": "^7.8.3",
+ "@babel/plugin-syntax-import-assertions": "^7.22.5",
+ "@babel/plugin-syntax-import-attributes": "^7.22.5",
+ "@babel/plugin-syntax-import-meta": "^7.10.4",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+ "@babel/plugin-syntax-top-level-await": "^7.14.5",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.22.5",
+ "@babel/plugin-transform-async-generator-functions": "^7.22.15",
+ "@babel/plugin-transform-async-to-generator": "^7.22.5",
+ "@babel/plugin-transform-block-scoped-functions": "^7.22.5",
+ "@babel/plugin-transform-block-scoping": "^7.22.15",
+ "@babel/plugin-transform-class-properties": "^7.22.5",
+ "@babel/plugin-transform-class-static-block": "^7.22.11",
+ "@babel/plugin-transform-classes": "^7.22.15",
+ "@babel/plugin-transform-computed-properties": "^7.22.5",
+ "@babel/plugin-transform-destructuring": "^7.22.15",
+ "@babel/plugin-transform-dotall-regex": "^7.22.5",
+ "@babel/plugin-transform-duplicate-keys": "^7.22.5",
+ "@babel/plugin-transform-dynamic-import": "^7.22.11",
+ "@babel/plugin-transform-exponentiation-operator": "^7.22.5",
+ "@babel/plugin-transform-export-namespace-from": "^7.22.11",
+ "@babel/plugin-transform-for-of": "^7.22.15",
+ "@babel/plugin-transform-function-name": "^7.22.5",
+ "@babel/plugin-transform-json-strings": "^7.22.11",
+ "@babel/plugin-transform-literals": "^7.22.5",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.22.11",
+ "@babel/plugin-transform-member-expression-literals": "^7.22.5",
+ "@babel/plugin-transform-modules-amd": "^7.22.5",
+ "@babel/plugin-transform-modules-commonjs": "^7.22.15",
+ "@babel/plugin-transform-modules-systemjs": "^7.22.11",
+ "@babel/plugin-transform-modules-umd": "^7.22.5",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5",
+ "@babel/plugin-transform-new-target": "^7.22.5",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11",
+ "@babel/plugin-transform-numeric-separator": "^7.22.11",
+ "@babel/plugin-transform-object-rest-spread": "^7.22.15",
+ "@babel/plugin-transform-object-super": "^7.22.5",
+ "@babel/plugin-transform-optional-catch-binding": "^7.22.11",
+ "@babel/plugin-transform-optional-chaining": "^7.22.15",
+ "@babel/plugin-transform-parameters": "^7.22.15",
+ "@babel/plugin-transform-private-methods": "^7.22.5",
+ "@babel/plugin-transform-private-property-in-object": "^7.22.11",
+ "@babel/plugin-transform-property-literals": "^7.22.5",
+ "@babel/plugin-transform-regenerator": "^7.22.10",
+ "@babel/plugin-transform-reserved-words": "^7.22.5",
+ "@babel/plugin-transform-shorthand-properties": "^7.22.5",
+ "@babel/plugin-transform-spread": "^7.22.5",
+ "@babel/plugin-transform-sticky-regex": "^7.22.5",
+ "@babel/plugin-transform-template-literals": "^7.22.5",
+ "@babel/plugin-transform-typeof-symbol": "^7.22.5",
+ "@babel/plugin-transform-unicode-escapes": "^7.22.10",
+ "@babel/plugin-transform-unicode-property-regex": "^7.22.5",
+ "@babel/plugin-transform-unicode-regex": "^7.22.5",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.22.5",
+ "@babel/preset-modules": "0.1.6-no-external-plugins",
+ "@babel/types": "^7.22.19",
+ "babel-plugin-polyfill-corejs2": "^0.4.5",
+ "babel-plugin-polyfill-corejs3": "^0.8.3",
+ "babel-plugin-polyfill-regenerator": "^0.5.2",
+ "core-js-compat": "^3.31.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.6-no-external-plugins",
+ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
+ "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/regjsgen": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz",
+ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==",
+ "dev": true
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.23.1",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz",
+ "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==",
+ "dev": true,
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true
+ },
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
+ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/console": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz",
+ "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz",
+ "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==",
+ "dev": true,
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/reporters": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-changed-files": "^29.7.0",
+ "jest-config": "^29.7.0",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-resolve-dependencies": "^29.7.0",
+ "jest-runner": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "jest-watcher": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/environment": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
+ "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/expect": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==",
+ "dev": true,
+ "dependencies": {
+ "expect": "^29.7.0",
+ "jest-snapshot": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/expect-utils": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
+ "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
+ "dev": true,
+ "dependencies": {
+ "jest-get-type": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/fake-timers": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz",
+ "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@types/node": "*",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/globals": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
+ "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
+ "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==",
+ "dev": true,
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "exit": "^0.1.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^6.0.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "slash": "^3.0.0",
+ "string-length": "^4.0.1",
+ "strip-ansi": "^6.0.0",
+ "v8-to-istanbul": "^9.0.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+ "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
+ "dev": true,
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/source-map": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz",
+ "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-result": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz",
+ "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz",
+ "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/test-result": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/transform": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
+ "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/types": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
+ "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+ "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+ "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.19",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
+ "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
+ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+ "dev": true
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
+ "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
+ "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz",
+ "integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.5.tgz",
+ "integrity": "sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz",
+ "integrity": "sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.2.tgz",
+ "integrity": "sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/graceful-fs": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.7.tgz",
+ "integrity": "sha512-MhzcwU8aUygZroVwL2jeYk6JisJrPl/oov/gsgGCue9mkgl9wjGbzReYQClxiUgFDnib9FuHqTndccKeZKxTRw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
+ "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
+ "dev": true
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-gPQuzaPR5h/djlAv2apEG1HVOyj1IUs7GpfMZixU0/0KXT3pm64ylHuMUI1/Akh+sq/iikxg6Z2j+fcMDXaaTQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.2.tgz",
+ "integrity": "sha512-kv43F9eb3Lhj+lr/Hn6OcLCs/sSM8bt+fIaP11rCYngfV6NVjzWXJ17owQtDQTL9tQ8WSLUrGsSJ6rJz0F1w1A==",
+ "dev": true,
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "node_modules/@types/jest": {
+ "version": "29.5.5",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz",
+ "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==",
+ "dev": true,
+ "dependencies": {
+ "expect": "^29.0.0",
+ "pretty-format": "^29.0.0"
+ }
+ },
+ "node_modules/@types/jsdom": {
+ "version": "20.0.1",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
+ "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "parse5": "^7.0.0"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "20.8.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.3.tgz",
+ "integrity": "sha512-jxiZQFpb+NlH5kjW49vXxvxTjeeqlbsnTAdBTKpzEdPs9itay7MscYXz3Fo9VYFEsfQ6LJFitHad3faerLAjCw==",
+ "dev": true
+ },
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
+ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
+ "dev": true
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz",
+ "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==",
+ "dev": true
+ },
+ "node_modules/@types/yargs": {
+ "version": "17.0.28",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.28.tgz",
+ "integrity": "sha512-N3e3fkS86hNhtk6BEnc0rj3zcehaxx8QWhCROJkqpl5Zaoi7nAic3jH8q94jVD3zu5LGk+PUB6KAiDmimYOEQw==",
+ "dev": true,
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.1.tgz",
+ "integrity": "sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==",
+ "dev": true
+ },
+ "node_modules/abab": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+ "dev": true
+ },
+ "node_modules/acorn": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-globals": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
+ "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.1.0",
+ "acorn-walk": "^8.0.2"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
+ "node_modules/babel-jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
+ "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
+ "dev": true,
+ "dependencies": {
+ "@jest/transform": "^29.7.0",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^29.6.3",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
+ "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
+ "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-jest-hoist": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
+ "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz",
+ "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.22.6",
+ "@babel/helper-define-polyfill-provider": "^0.4.2",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz",
+ "integrity": "sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.4.2",
+ "core-js-compat": "^3.32.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz",
+ "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.4.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-preset-current-node-syntax": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
+ "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.8.3",
+ "@babel/plugin-syntax-import-meta": "^7.8.3",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.8.3",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-top-level-await": "^7.8.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-preset-jest": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
+ "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
+ "dev": true,
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
+ "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001541",
+ "electron-to-chromium": "^1.4.535",
+ "node-releases": "^2.0.13",
+ "update-browserslist-db": "^1.0.13"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bser": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
+ "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
+ "dev": true,
+ "dependencies": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001546",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz",
+ "integrity": "sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/char-regex": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",
+ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==",
+ "dev": true
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+ "dev": true,
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/collect-v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==",
+ "dev": true
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
+ },
+ "node_modules/core-js-compat": {
+ "version": "3.33.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz",
+ "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==",
+ "dev": true,
+ "dependencies": {
+ "browserslist": "^4.22.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/create-jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
+ "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-config": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "prompts": "^2.0.1"
+ },
+ "bin": {
+ "create-jest": "bin/create-jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+ "dev": true
+ },
+ "node_modules/cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "dev": true,
+ "dependencies": {
+ "cssom": "~0.3.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cssstyle/node_modules/cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "dev": true
+ },
+ "node_modules/data-urls": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+ "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+ "dev": true,
+ "dependencies": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
+ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
+ "dev": true
+ },
+ "node_modules/dedent": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz",
+ "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==",
+ "dev": true,
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
+ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
+ "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
+ "dev": true,
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/domexception": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+ "dev": true,
+ "dependencies": {
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.544",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.544.tgz",
+ "integrity": "sha512-54z7squS1FyFRSUqq/knOFSptjjogLZXbKcYk3B0qkE1KZzvqASwRZnY2KzZQJqIYLVD38XZeoiMRflYSwyO4w==",
+ "dev": true
+ },
+ "node_modules/emittery": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
+ "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dev": true,
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expect": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fb-watchman": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
+ "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
+ "dev": true,
+ "dependencies": {
+ "bser": "2.1.1"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
+ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true
+ },
+ "node_modules/has": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
+ "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
+ "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
+ "dev": true,
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true
+ },
+ "node_modules/is-core-module": {
+ "version": "2.13.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
+ "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
+ "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz",
+ "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-instrument/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-instrument/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-instrument/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz",
+ "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==",
+ "dev": true,
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
+ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/core": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "import-local": "^3.0.2",
+ "jest-cli": "^29.7.0"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-changed-files": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz",
+ "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==",
+ "dev": true,
+ "dependencies": {
+ "execa": "^5.0.0",
+ "jest-util": "^29.7.0",
+ "p-limit": "^3.1.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz",
+ "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "dedent": "^1.0.0",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^29.7.0",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "p-limit": "^3.1.0",
+ "pretty-format": "^29.7.0",
+ "pure-rand": "^6.0.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz",
+ "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==",
+ "dev": true,
+ "dependencies": {
+ "@jest/core": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "create-jest": "^29.7.0",
+ "exit": "^0.1.2",
+ "import-local": "^3.0.2",
+ "jest-config": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "yargs": "^17.3.1"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-config": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz",
+ "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/test-sequencer": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-jest": "^29.7.0",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "deepmerge": "^4.2.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-circus": "^29.7.0",
+ "jest-environment-node": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-runner": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "parse-json": "^5.2.0",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-diff": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
+ "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-docblock": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz",
+ "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==",
+ "dev": true,
+ "dependencies": {
+ "detect-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-each": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz",
+ "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz",
+ "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/jsdom": "^20.0.0",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jsdom": "^20.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-environment-node": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
+ "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
+ "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
+ "dev": true,
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-haste-map": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz",
+ "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-leak-detector": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz",
+ "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==",
+ "dev": true,
+ "dependencies": {
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
+ "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
+ "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-mock": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
+ "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+ "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-regex-util": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz",
+ "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==",
+ "dev": true,
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz",
+ "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^2.0.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz",
+ "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==",
+ "dev": true,
+ "dependencies": {
+ "jest-regex-util": "^29.6.3",
+ "jest-snapshot": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz",
+ "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/environment": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "emittery": "^0.13.1",
+ "graceful-fs": "^4.2.9",
+ "jest-docblock": "^29.7.0",
+ "jest-environment-node": "^29.7.0",
+ "jest-haste-map": "^29.7.0",
+ "jest-leak-detector": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-resolve": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-watcher": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "p-limit": "^3.1.0",
+ "source-map-support": "0.5.13"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runtime": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz",
+ "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/globals": "^29.7.0",
+ "@jest/source-map": "^29.6.3",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-snapshot": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
+ "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-jsx": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^29.7.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/jest-util": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
+ "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-validate": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz",
+ "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "leven": "^3.1.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-validate/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watcher": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz",
+ "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==",
+ "dev": true,
+ "dependencies": {
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "emittery": "^0.13.1",
+ "jest-util": "^29.7.0",
+ "string-length": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
+ "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.7.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "20.0.3",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
+ "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
+ "dev": true,
+ "dependencies": {
+ "abab": "^2.0.6",
+ "acorn": "^8.8.1",
+ "acorn-globals": "^7.0.0",
+ "cssom": "^0.5.0",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^3.0.2",
+ "decimal.js": "^10.4.2",
+ "domexception": "^4.0.0",
+ "escodegen": "^2.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.2",
+ "parse5": "^7.1.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.1.2",
+ "w3c-xmlserializer": "^4.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0",
+ "ws": "^8.11.0",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "dev": true
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/make-dir/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/makeerror": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
+ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+ "dev": true,
+ "dependencies": {
+ "tmpl": "1.0.5"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+ "dev": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
+ "dev": true
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
+ "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==",
+ "dev": true
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-locate/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
+ "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
+ "dev": true,
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/psl": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
+ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
+ "dev": true
+ },
+ "node_modules/punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pure-rand": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz",
+ "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ]
+ },
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "dev": true
+ },
+ "node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
+ "dev": true
+ },
+ "node_modules/regenerate": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
+ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
+ "dev": true
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz",
+ "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==",
+ "dev": true,
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
+ "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
+ "dev": true
+ },
+ "node_modules/regenerator-transform": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
+ "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.8.4"
+ }
+ },
+ "node_modules/regexpu-core": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz",
+ "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/regjsgen": "^0.8.0",
+ "regenerate": "^1.4.2",
+ "regenerate-unicode-properties": "^10.1.0",
+ "regjsparser": "^0.9.1",
+ "unicode-match-property-ecmascript": "^2.0.0",
+ "unicode-match-property-value-ecmascript": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regjsparser": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz",
+ "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==",
+ "dev": true,
+ "dependencies": {
+ "jsesc": "~0.5.0"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/regjsparser/node_modules/jsesc": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+ "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true
+ },
+ "node_modules/resolve": {
+ "version": "1.22.6",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz",
+ "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve.exports": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz",
+ "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "dev": true
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
+ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/string-length": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+ "dev": true,
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "dev": true,
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tmpl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+ "dev": true
+ },
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
+ "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
+ "dev": true,
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/unicode-canonical-property-names-ecmascript": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
+ "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-ecmascript": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
+ "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
+ "dev": true,
+ "dependencies": {
+ "unicode-canonical-property-names-ecmascript": "^2.0.0",
+ "unicode-property-aliases-ecmascript": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-value-ecmascript": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz",
+ "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-property-aliases-ecmascript": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
+ "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
+ "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dev": true,
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "node_modules/v8-to-istanbul": {
+ "version": "9.1.3",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz",
+ "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
+ "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
+ "dev": true,
+ "dependencies": {
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/walker": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+ "dev": true,
+ "dependencies": {
+ "makeerror": "1.0.12"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "dev": true,
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "node_modules/write-file-atomic": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
+ "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.7"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.14.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
+ "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/ComfyUI/tests-ui/package.json b/ComfyUI/tests-ui/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..e7b60ad8e7513c950b0b5329b9302e646ce61b50
--- /dev/null
+++ b/ComfyUI/tests-ui/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "comfui-tests",
+ "version": "1.0.0",
+ "description": "UI tests",
+ "main": "index.js",
+ "scripts": {
+ "test": "jest",
+ "test:generate": "node setup.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/comfyanonymous/ComfyUI.git"
+ },
+ "keywords": [
+ "comfyui",
+ "test"
+ ],
+ "author": "comfyanonymous",
+ "license": "GPL-3.0",
+ "bugs": {
+ "url": "https://github.com/comfyanonymous/ComfyUI/issues"
+ },
+ "homepage": "https://github.com/comfyanonymous/ComfyUI#readme",
+ "devDependencies": {
+ "@babel/preset-env": "^7.22.20",
+ "@types/jest": "^29.5.5",
+ "jest": "^29.7.0",
+ "jest-environment-jsdom": "^29.7.0"
+ }
+}
diff --git a/ComfyUI/tests-ui/setup.js b/ComfyUI/tests-ui/setup.js
new file mode 100644
index 0000000000000000000000000000000000000000..8bbd9dcdf20cde46f1f2c10591d9c9415a71391a
--- /dev/null
+++ b/ComfyUI/tests-ui/setup.js
@@ -0,0 +1,88 @@
+const { spawn } = require("child_process");
+const { resolve } = require("path");
+const { existsSync, mkdirSync, writeFileSync } = require("fs");
+const http = require("http");
+
+async function setup() {
+ // Wait up to 30s for it to start
+ let success = false;
+ let child;
+ for (let i = 0; i < 30; i++) {
+ try {
+ await new Promise((res, rej) => {
+ http
+ .get("http://127.0.0.1:8188/object_info", (resp) => {
+ let data = "";
+ resp.on("data", (chunk) => {
+ data += chunk;
+ });
+ resp.on("end", () => {
+ // Modify the response data to add some checkpoints
+ const objectInfo = JSON.parse(data);
+ objectInfo.CheckpointLoaderSimple.input.required.ckpt_name[0] = ["model1.safetensors", "model2.ckpt"];
+ objectInfo.VAELoader.input.required.vae_name[0] = ["vae1.safetensors", "vae2.ckpt"];
+
+ data = JSON.stringify(objectInfo, undefined, "\t");
+
+ const outDir = resolve("./data");
+ if (!existsSync(outDir)) {
+ mkdirSync(outDir);
+ }
+
+ const outPath = resolve(outDir, "object_info.json");
+ console.log(`Writing ${Object.keys(objectInfo).length} nodes to ${outPath}`);
+ writeFileSync(outPath, data, {
+ encoding: "utf8",
+ });
+ res();
+ });
+ })
+ .on("error", rej);
+ });
+ success = true;
+ break;
+ } catch (error) {
+ console.log(i + "/30", error);
+ if (i === 0) {
+ // Start the server on first iteration if it fails to connect
+ console.log("Starting ComfyUI server...");
+
+ let python = resolve("../../python_embeded/python.exe");
+ let args;
+ let cwd;
+ if (existsSync(python)) {
+ args = ["-s", "ComfyUI/main.py"];
+ cwd = "../..";
+ } else {
+ python = "python";
+ args = ["main.py"];
+ cwd = "..";
+ }
+ args.push("--cpu");
+ console.log(python, ...args);
+ child = spawn(python, args, { cwd });
+ child.on("error", (err) => {
+ console.log(`Server error (${err})`);
+ i = 30;
+ });
+ child.on("exit", (code) => {
+ if (!success) {
+ console.log(`Server exited (${code})`);
+ i = 30;
+ }
+ });
+ }
+ await new Promise((r) => {
+ setTimeout(r, 1000);
+ });
+ }
+ }
+
+ child?.kill();
+
+ if (!success) {
+ throw new Error("Waiting for server failed...");
+ }
+}
+
+ setup();
\ No newline at end of file
diff --git a/ComfyUI/tests-ui/tests/extensions.test.js b/ComfyUI/tests-ui/tests/extensions.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..159e5113a293695532b7a73c1d3add754b364039
--- /dev/null
+++ b/ComfyUI/tests-ui/tests/extensions.test.js
@@ -0,0 +1,196 @@
+// @ts-check
+///
+const { start } = require("../utils");
+const lg = require("../utils/litegraph");
+
+describe("extensions", () => {
+ beforeEach(() => {
+ lg.setup(global);
+ });
+
+ afterEach(() => {
+ lg.teardown(global);
+ });
+
+ it("calls each extension hook", async () => {
+ const mockExtension = {
+ name: "TestExtension",
+ init: jest.fn(),
+ setup: jest.fn(),
+ addCustomNodeDefs: jest.fn(),
+ getCustomWidgets: jest.fn(),
+ beforeRegisterNodeDef: jest.fn(),
+ registerCustomNodes: jest.fn(),
+ loadedGraphNode: jest.fn(),
+ nodeCreated: jest.fn(),
+ beforeConfigureGraph: jest.fn(),
+ afterConfigureGraph: jest.fn(),
+ };
+
+ const { app, ez, graph } = await start({
+ async preSetup(app) {
+ app.registerExtension(mockExtension);
+ },
+ });
+
+ // Basic initialisation hooks should be called once, with app
+ expect(mockExtension.init).toHaveBeenCalledTimes(1);
+ expect(mockExtension.init).toHaveBeenCalledWith(app);
+
+ // Adding custom node defs should be passed the full list of nodes
+ expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
+ expect(mockExtension.addCustomNodeDefs.mock.calls[0][1]).toStrictEqual(app);
+ const defs = mockExtension.addCustomNodeDefs.mock.calls[0][0];
+ expect(defs).toHaveProperty("KSampler");
+ expect(defs).toHaveProperty("LoadImage");
+
+ // Get custom widgets is called once and should return new widget types
+ expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
+ expect(mockExtension.getCustomWidgets).toHaveBeenCalledWith(app);
+
+ // Before register node def will be called once per node type
+ const nodeNames = Object.keys(defs);
+ const nodeCount = nodeNames.length;
+ expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
+ for (let i = 0; i < 10; i++) {
+ // It should be send the JS class and the original JSON definition
+ const nodeClass = mockExtension.beforeRegisterNodeDef.mock.calls[i][0];
+ const nodeDef = mockExtension.beforeRegisterNodeDef.mock.calls[i][1];
+
+ expect(nodeClass.name).toBe("ComfyNode");
+ expect(nodeClass.comfyClass).toBe(nodeNames[i]);
+ expect(nodeDef.name).toBe(nodeNames[i]);
+ expect(nodeDef).toHaveProperty("input");
+ expect(nodeDef).toHaveProperty("output");
+ }
+
+ // Register custom nodes is called once after registerNode defs to allow adding other frontend nodes
+ expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
+
+ // Before configure graph will be called here as the default graph is being loaded
+ expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(1);
+ // it gets sent the graph data that is going to be loaded
+ const graphData = mockExtension.beforeConfigureGraph.mock.calls[0][0];
+
+ // A node created is fired for each node constructor that is called
+ expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length);
+ for (let i = 0; i < graphData.nodes.length; i++) {
+ expect(mockExtension.nodeCreated.mock.calls[i][0].type).toBe(graphData.nodes[i].type);
+ }
+
+ // Each node then calls loadedGraphNode to allow them to be updated
+ expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length);
+ for (let i = 0; i < graphData.nodes.length; i++) {
+ expect(mockExtension.loadedGraphNode.mock.calls[i][0].type).toBe(graphData.nodes[i].type);
+ }
+
+ // After configure is then called once all the setup is done
+ expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(1);
+
+ expect(mockExtension.setup).toHaveBeenCalledTimes(1);
+ expect(mockExtension.setup).toHaveBeenCalledWith(app);
+
+ // Ensure hooks are called in the correct order
+ const callOrder = [
+ "init",
+ "addCustomNodeDefs",
+ "getCustomWidgets",
+ "beforeRegisterNodeDef",
+ "registerCustomNodes",
+ "beforeConfigureGraph",
+ "nodeCreated",
+ "loadedGraphNode",
+ "afterConfigureGraph",
+ "setup",
+ ];
+ for (let i = 1; i < callOrder.length; i++) {
+ const fn1 = mockExtension[callOrder[i - 1]];
+ const fn2 = mockExtension[callOrder[i]];
+ expect(fn1.mock.invocationCallOrder[0]).toBeLessThan(fn2.mock.invocationCallOrder[0]);
+ }
+
+ graph.clear();
+
+ // Ensure adding a new node calls the correct callback
+ ez.LoadImage();
+ expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length);
+ expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 1);
+ expect(mockExtension.nodeCreated.mock.lastCall[0].type).toBe("LoadImage");
+
+ // Reload the graph to ensure correct hooks are fired
+ await graph.reload();
+
+ // These hooks should not be fired again
+ expect(mockExtension.init).toHaveBeenCalledTimes(1);
+ expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
+ expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
+ expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
+ expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
+ expect(mockExtension.setup).toHaveBeenCalledTimes(1);
+
+ // These should be called again
+ expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(2);
+ expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 2);
+ expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length + 1);
+ expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(2);
+ }, 15000);
+
+ it("allows custom nodeDefs and widgets to be registered", async () => {
+ const widgetMock = jest.fn((node, inputName, inputData, app) => {
+ expect(node.constructor.comfyClass).toBe("TestNode");
+ expect(inputName).toBe("test_input");
+ expect(inputData[0]).toBe("CUSTOMWIDGET");
+ expect(inputData[1]?.hello).toBe("world");
+ expect(app).toStrictEqual(app);
+
+ return {
+ widget: node.addWidget("button", inputName, "hello", () => {}),
+ };
+ });
+
+ // Register our extension that adds a custom node + widget type
+ const mockExtension = {
+ name: "TestExtension",
+ addCustomNodeDefs: (nodeDefs) => {
+ nodeDefs["TestNode"] = {
+ output: [],
+ output_name: [],
+ output_is_list: [],
+ name: "TestNode",
+ display_name: "TestNode",
+ category: "Test",
+ input: {
+ required: {
+ test_input: ["CUSTOMWIDGET", { hello: "world" }],
+ },
+ },
+ };
+ },
+ getCustomWidgets: jest.fn(() => {
+ return {
+ CUSTOMWIDGET: widgetMock,
+ };
+ }),
+ };
+
+ const { graph, ez } = await start({
+ async preSetup(app) {
+ app.registerExtension(mockExtension);
+ },
+ });
+
+ expect(mockExtension.getCustomWidgets).toBeCalledTimes(1);
+
+ graph.clear();
+ expect(widgetMock).toBeCalledTimes(0);
+ const node = ez.TestNode();
+ expect(widgetMock).toBeCalledTimes(1);
+
+ // Ensure our custom widget is created
+ expect(node.inputs.length).toBe(0);
+ expect(node.widgets.length).toBe(1);
+ const w = node.widgets[0].widget;
+ expect(w.name).toBe("test_input");
+ expect(w.type).toBe("button");
+ });
+});
diff --git a/ComfyUI/tests-ui/tests/groupNode.test.js b/ComfyUI/tests-ui/tests/groupNode.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6ebedd9150a2d0ea0ef4b4a522e1a3a85ddbc57
--- /dev/null
+++ b/ComfyUI/tests-ui/tests/groupNode.test.js
@@ -0,0 +1,1005 @@
+// @ts-check
+///
+
+const { start, createDefaultWorkflow, getNodeDef, checkBeforeAndAfterReload } = require("../utils");
+const lg = require("../utils/litegraph");
+
+describe("group node", () => {
+ beforeEach(() => {
+ lg.setup(global);
+ });
+
+ afterEach(() => {
+ lg.teardown(global);
+ });
+
+ /**
+ *
+ * @param {*} app
+ * @param {*} graph
+ * @param {*} name
+ * @param {*} nodes
+ * @returns { Promise> }
+ */
+ async function convertToGroup(app, graph, name, nodes) {
+ // Select the nodes we are converting
+ for (const n of nodes) {
+ n.select(true);
+ }
+
+ expect(Object.keys(app.canvas.selected_nodes).sort((a, b) => +a - +b)).toEqual(
+ nodes.map((n) => n.id + "").sort((a, b) => +a - +b)
+ );
+
+ global.prompt = jest.fn().mockImplementation(() => name);
+ const groupNode = await nodes[0].menu["Convert to Group Node"].call(false);
+
+ // Check group name was requested
+ expect(window.prompt).toHaveBeenCalled();
+
+ // Ensure old nodes are removed
+ for (const n of nodes) {
+ expect(n.isRemoved).toBeTruthy();
+ }
+
+ expect(groupNode.type).toEqual("workflow/" + name);
+
+ return graph.find(groupNode);
+ }
+
+ /**
+ * @param { Record | number[] } idMap
+ * @param { Record> } valueMap
+ */
+ function getOutput(idMap = {}, valueMap = {}) {
+ if (idMap instanceof Array) {
+ idMap = idMap.reduce((p, n) => {
+ p[n] = n + "";
+ return p;
+ }, {});
+ }
+ const expected = {
+ 1: { inputs: { ckpt_name: "model1.safetensors", ...valueMap?.[1] }, class_type: "CheckpointLoaderSimple" },
+ 2: { inputs: { text: "positive", clip: ["1", 1], ...valueMap?.[2] }, class_type: "CLIPTextEncode" },
+ 3: { inputs: { text: "negative", clip: ["1", 1], ...valueMap?.[3] }, class_type: "CLIPTextEncode" },
+ 4: { inputs: { width: 512, height: 512, batch_size: 1, ...valueMap?.[4] }, class_type: "EmptyLatentImage" },
+ 5: {
+ inputs: {
+ seed: 0,
+ steps: 20,
+ cfg: 8,
+ sampler_name: "euler",
+ scheduler: "normal",
+ denoise: 1,
+ model: ["1", 0],
+ positive: ["2", 0],
+ negative: ["3", 0],
+ latent_image: ["4", 0],
+ ...valueMap?.[5],
+ },
+ class_type: "KSampler",
+ },
+ 6: { inputs: { samples: ["5", 0], vae: ["1", 2], ...valueMap?.[6] }, class_type: "VAEDecode" },
+ 7: { inputs: { filename_prefix: "ComfyUI", images: ["6", 0], ...valueMap?.[7] }, class_type: "SaveImage" },
+ };
+
+ // Map old IDs to new at the top level
+ const mapped = {};
+ for (const oldId in idMap) {
+ mapped[idMap[oldId]] = expected[oldId];
+ delete expected[oldId];
+ }
+ Object.assign(mapped, expected);
+
+ // Map old IDs to new inside links
+ for (const k in mapped) {
+ for (const input in mapped[k].inputs) {
+ const v = mapped[k].inputs[input];
+ if (v instanceof Array) {
+ if (v[0] in idMap) {
+ v[0] = idMap[v[0]] + "";
+ }
+ }
+ }
+ }
+
+ return mapped;
+ }
+
+ test("can be created from selected nodes", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+ const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg, nodes.empty]);
+
+ // Ensure links are now to the group node
+ expect(group.inputs).toHaveLength(2);
+ expect(group.outputs).toHaveLength(3);
+
+ expect(group.inputs.map((i) => i.input.name)).toEqual(["clip", "CLIPTextEncode clip"]);
+ expect(group.outputs.map((i) => i.output.name)).toEqual(["LATENT", "CONDITIONING", "CLIPTextEncode CONDITIONING"]);
+
+ // ckpt clip to both clip inputs on the group
+ expect(nodes.ckpt.outputs.CLIP.connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
+ [group.id, 0],
+ [group.id, 1],
+ ]);
+
+ // group conditioning to sampler
+ expect(group.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
+ [nodes.sampler.id, 1],
+ ]);
+ // group conditioning 2 to sampler
+ expect(
+ group.outputs["CLIPTextEncode CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])
+ ).toEqual([[nodes.sampler.id, 2]]);
+ // group latent to sampler
+ expect(group.outputs["LATENT"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
+ [nodes.sampler.id, 3],
+ ]);
+ });
+
+ test("maintains all output links on conversion", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+ const save2 = ez.SaveImage(...nodes.decode.outputs);
+ const save3 = ez.SaveImage(...nodes.decode.outputs);
+ // Ensure an output with multiple links maintains them on convert to group
+ const group = await convertToGroup(app, graph, "test", [nodes.sampler, nodes.decode]);
+ expect(group.outputs[0].connections.length).toBe(3);
+ expect(group.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id);
+ expect(group.outputs[0].connections[1].targetNode.id).toBe(save2.id);
+ expect(group.outputs[0].connections[2].targetNode.id).toBe(save3.id);
+
+ // and they're still linked when converting back to nodes
+ const newNodes = group.menu["Convert to nodes"].call();
+ const decode = graph.find(newNodes.find((n) => n.type === "VAEDecode"));
+ expect(decode.outputs[0].connections.length).toBe(3);
+ expect(decode.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id);
+ expect(decode.outputs[0].connections[1].targetNode.id).toBe(save2.id);
+ expect(decode.outputs[0].connections[2].targetNode.id).toBe(save3.id);
+ });
+ test("can be be converted back to nodes", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+ const toConvert = [nodes.pos, nodes.neg, nodes.empty, nodes.sampler];
+ const group = await convertToGroup(app, graph, "test", toConvert);
+
+ // Edit some values to ensure they are set back onto the converted nodes
+ expect(group.widgets["text"].value).toBe("positive");
+ group.widgets["text"].value = "pos";
+ expect(group.widgets["CLIPTextEncode text"].value).toBe("negative");
+ group.widgets["CLIPTextEncode text"].value = "neg";
+ expect(group.widgets["width"].value).toBe(512);
+ group.widgets["width"].value = 1024;
+ expect(group.widgets["sampler_name"].value).toBe("euler");
+ group.widgets["sampler_name"].value = "ddim";
+ expect(group.widgets["control_after_generate"].value).toBe("randomize");
+ group.widgets["control_after_generate"].value = "fixed";
+
+ /** @type { Array } */
+ group.menu["Convert to nodes"].call();
+
+ // ensure widget values are set
+ const pos = graph.find(nodes.pos.id);
+ expect(pos.node.type).toBe("CLIPTextEncode");
+ expect(pos.widgets["text"].value).toBe("pos");
+ const neg = graph.find(nodes.neg.id);
+ expect(neg.node.type).toBe("CLIPTextEncode");
+ expect(neg.widgets["text"].value).toBe("neg");
+ const empty = graph.find(nodes.empty.id);
+ expect(empty.node.type).toBe("EmptyLatentImage");
+ expect(empty.widgets["width"].value).toBe(1024);
+ const sampler = graph.find(nodes.sampler.id);
+ expect(sampler.node.type).toBe("KSampler");
+ expect(sampler.widgets["sampler_name"].value).toBe("ddim");
+ expect(sampler.widgets["control_after_generate"].value).toBe("fixed");
+
+ // validate links
+ expect(nodes.ckpt.outputs.CLIP.connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
+ [pos.id, 0],
+ [neg.id, 0],
+ ]);
+
+ expect(pos.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
+ [nodes.sampler.id, 1],
+ ]);
+
+ expect(neg.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
+ [nodes.sampler.id, 2],
+ ]);
+
+ expect(empty.outputs["LATENT"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
+ [nodes.sampler.id, 3],
+ ]);
+ });
+ test("it can embed reroutes as inputs", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+
+ // Add and connect a reroute to the clip text encodes
+ const reroute = ez.Reroute();
+ nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]);
+ reroute.outputs[0].connectTo(nodes.pos.inputs[0]);
+ reroute.outputs[0].connectTo(nodes.neg.inputs[0]);
+
+ // Convert to group and ensure we only have 1 input of the correct type
+ const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg, nodes.empty, reroute]);
+ expect(group.inputs).toHaveLength(1);
+ expect(group.inputs[0].input.type).toEqual("CLIP");
+
+ expect((await graph.toPrompt()).output).toEqual(getOutput());
+ });
+ test("it can embed reroutes as outputs", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+
+ // Add a reroute with no output so we output IMAGE even though its used internally
+ const reroute = ez.Reroute();
+ nodes.decode.outputs.IMAGE.connectTo(reroute.inputs[0]);
+
+ // Convert to group and ensure there is an IMAGE output
+ const group = await convertToGroup(app, graph, "test", [nodes.decode, nodes.save, reroute]);
+ expect(group.outputs).toHaveLength(1);
+ expect(group.outputs[0].output.type).toEqual("IMAGE");
+ expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.decode.id, nodes.save.id]));
+ });
+ test("it can embed reroutes as pipes", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+
+ // Use reroutes as a pipe
+ const rerouteModel = ez.Reroute();
+ const rerouteClip = ez.Reroute();
+ const rerouteVae = ez.Reroute();
+ nodes.ckpt.outputs.MODEL.connectTo(rerouteModel.inputs[0]);
+ nodes.ckpt.outputs.CLIP.connectTo(rerouteClip.inputs[0]);
+ nodes.ckpt.outputs.VAE.connectTo(rerouteVae.inputs[0]);
+
+ const group = await convertToGroup(app, graph, "test", [rerouteModel, rerouteClip, rerouteVae]);
+
+ expect(group.outputs).toHaveLength(3);
+ expect(group.outputs.map((o) => o.output.type)).toEqual(["MODEL", "CLIP", "VAE"]);
+
+ expect(group.outputs).toHaveLength(3);
+ expect(group.outputs.map((o) => o.output.type)).toEqual(["MODEL", "CLIP", "VAE"]);
+
+ group.outputs[0].connectTo(nodes.sampler.inputs.model);
+ group.outputs[1].connectTo(nodes.pos.inputs.clip);
+ group.outputs[1].connectTo(nodes.neg.inputs.clip);
+ });
+ test("can handle reroutes used internally", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+
+ let reroutes = [];
+ let prevNode = nodes.ckpt;
+ for (let i = 0; i < 5; i++) {
+ const reroute = ez.Reroute();
+ prevNode.outputs[0].connectTo(reroute.inputs[0]);
+ prevNode = reroute;
+ reroutes.push(reroute);
+ }
+ prevNode.outputs[0].connectTo(nodes.sampler.inputs.model);
+
+ const group = await convertToGroup(app, graph, "test", [...reroutes, ...Object.values(nodes)]);
+ expect((await graph.toPrompt()).output).toEqual(getOutput());
+
+ group.menu["Convert to nodes"].call();
+ expect((await graph.toPrompt()).output).toEqual(getOutput());
+ });
+ test("creates with widget values from inner nodes", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+
+ nodes.ckpt.widgets.ckpt_name.value = "model2.ckpt";
+ nodes.pos.widgets.text.value = "hello";
+ nodes.neg.widgets.text.value = "world";
+ nodes.empty.widgets.width.value = 256;
+ nodes.empty.widgets.height.value = 1024;
+ nodes.sampler.widgets.seed.value = 1;
+ nodes.sampler.widgets.control_after_generate.value = "increment";
+ nodes.sampler.widgets.steps.value = 8;
+ nodes.sampler.widgets.cfg.value = 4.5;
+ nodes.sampler.widgets.sampler_name.value = "uni_pc";
+ nodes.sampler.widgets.scheduler.value = "karras";
+ nodes.sampler.widgets.denoise.value = 0.9;
+
+ const group = await convertToGroup(app, graph, "test", [
+ nodes.ckpt,
+ nodes.pos,
+ nodes.neg,
+ nodes.empty,
+ nodes.sampler,
+ ]);
+
+ expect(group.widgets["ckpt_name"].value).toEqual("model2.ckpt");
+ expect(group.widgets["text"].value).toEqual("hello");
+ expect(group.widgets["CLIPTextEncode text"].value).toEqual("world");
+ expect(group.widgets["width"].value).toEqual(256);
+ expect(group.widgets["height"].value).toEqual(1024);
+ expect(group.widgets["seed"].value).toEqual(1);
+ expect(group.widgets["control_after_generate"].value).toEqual("increment");
+ expect(group.widgets["steps"].value).toEqual(8);
+ expect(group.widgets["cfg"].value).toEqual(4.5);
+ expect(group.widgets["sampler_name"].value).toEqual("uni_pc");
+ expect(group.widgets["scheduler"].value).toEqual("karras");
+ expect(group.widgets["denoise"].value).toEqual(0.9);
+
+ expect((await graph.toPrompt()).output).toEqual(
+ getOutput([nodes.ckpt.id, nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id], {
+ [nodes.ckpt.id]: { ckpt_name: "model2.ckpt" },
+ [nodes.pos.id]: { text: "hello" },
+ [nodes.neg.id]: { text: "world" },
+ [nodes.empty.id]: { width: 256, height: 1024 },
+ [nodes.sampler.id]: {
+ seed: 1,
+ steps: 8,
+ cfg: 4.5,
+ sampler_name: "uni_pc",
+ scheduler: "karras",
+ denoise: 0.9,
+ },
+ })
+ );
+ });
+ test("group inputs can be reroutes", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+ const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
+
+ const reroute = ez.Reroute();
+ nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]);
+
+ reroute.outputs[0].connectTo(group.inputs[0]);
+ reroute.outputs[0].connectTo(group.inputs[1]);
+
+ expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id]));
+ });
+ test("group outputs can be reroutes", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+ const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
+
+ const reroute1 = ez.Reroute();
+ const reroute2 = ez.Reroute();
+ group.outputs[0].connectTo(reroute1.inputs[0]);
+ group.outputs[1].connectTo(reroute2.inputs[0]);
+
+ reroute1.outputs[0].connectTo(nodes.sampler.inputs.positive);
+ reroute2.outputs[0].connectTo(nodes.sampler.inputs.negative);
+
+ expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id]));
+ });
+ test("groups can connect to each other", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+ const group1 = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
+ const group2 = await convertToGroup(app, graph, "test2", [nodes.empty, nodes.sampler]);
+
+ group1.outputs[0].connectTo(group2.inputs["positive"]);
+ group1.outputs[1].connectTo(group2.inputs["negative"]);
+
+ expect((await graph.toPrompt()).output).toEqual(
+ getOutput([nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id])
+ );
+ });
+ test("groups can connect to each other via internal reroutes", async () => {
+ const { ez, graph, app } = await start();
+
+ const latent = ez.EmptyLatentImage();
+ const vae = ez.VAELoader();
+ const latentReroute = ez.Reroute();
+ const vaeReroute = ez.Reroute();
+
+ latent.outputs[0].connectTo(latentReroute.inputs[0]);
+ vae.outputs[0].connectTo(vaeReroute.inputs[0]);
+
+ const group1 = await convertToGroup(app, graph, "test", [latentReroute, vaeReroute]);
+ group1.menu.Clone.call();
+ expect(app.graph._nodes).toHaveLength(4);
+ const group2 = graph.find(app.graph._nodes[3]);
+ expect(group2.node.type).toEqual("workflow/test");
+ expect(group2.id).not.toEqual(group1.id);
+
+ group1.outputs.VAE.connectTo(group2.inputs.VAE);
+ group1.outputs.LATENT.connectTo(group2.inputs.LATENT);
+
+ const decode = ez.VAEDecode(group2.outputs.LATENT, group2.outputs.VAE);
+ const preview = ez.PreviewImage(decode.outputs[0]);
+
+ const output = {
+ [latent.id]: { inputs: { width: 512, height: 512, batch_size: 1 }, class_type: "EmptyLatentImage" },
+ [vae.id]: { inputs: { vae_name: "vae1.safetensors" }, class_type: "VAELoader" },
+ [decode.id]: { inputs: { samples: [latent.id + "", 0], vae: [vae.id + "", 0] }, class_type: "VAEDecode" },
+ [preview.id]: { inputs: { images: [decode.id + "", 0] }, class_type: "PreviewImage" },
+ };
+ expect((await graph.toPrompt()).output).toEqual(output);
+
+ // Ensure missing connections dont cause errors
+ group2.inputs.VAE.disconnect();
+ delete output[decode.id].inputs.vae;
+ expect((await graph.toPrompt()).output).toEqual(output);
+ });
+ test("displays generated image on group node", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+ let group = await convertToGroup(app, graph, "test", [
+ nodes.pos,
+ nodes.neg,
+ nodes.empty,
+ nodes.sampler,
+ nodes.decode,
+ nodes.save,
+ ]);
+
+ const { api } = require("../../web/scripts/api");
+
+ api.dispatchEvent(new CustomEvent("execution_start", {}));
+ api.dispatchEvent(new CustomEvent("executing", { detail: `${nodes.save.id}` }));
+ // Event should be forwarded to group node id
+ expect(+app.runningNodeId).toEqual(group.id);
+ expect(group.node["imgs"]).toBeFalsy();
+ api.dispatchEvent(
+ new CustomEvent("executed", {
+ detail: {
+ node: `${nodes.save.id}`,
+ output: {
+ images: [
+ {
+ filename: "test.png",
+ type: "output",
+ },
+ ],
+ },
+ },
+ })
+ );
+
+ // Trigger paint
+ group.node.onDrawBackground?.(app.canvas.ctx, app.canvas.canvas);
+
+ expect(group.node["images"]).toEqual([
+ {
+ filename: "test.png",
+ type: "output",
+ },
+ ]);
+
+ // Reload
+ const workflow = JSON.stringify((await graph.toPrompt()).workflow);
+ await app.loadGraphData(JSON.parse(workflow));
+ group = graph.find(group);
+
+ // Trigger inner nodes to get created
+ group.node["getInnerNodes"]();
+
+ // Check it works for internal node ids
+ api.dispatchEvent(new CustomEvent("execution_start", {}));
+ api.dispatchEvent(new CustomEvent("executing", { detail: `${group.id}:5` }));
+ // Event should be forwarded to group node id
+ expect(+app.runningNodeId).toEqual(group.id);
+ expect(group.node["imgs"]).toBeFalsy();
+ api.dispatchEvent(
+ new CustomEvent("executed", {
+ detail: {
+ node: `${group.id}:5`,
+ output: {
+ images: [
+ {
+ filename: "test2.png",
+ type: "output",
+ },
+ ],
+ },
+ },
+ })
+ );
+
+ // Trigger paint
+ group.node.onDrawBackground?.(app.canvas.ctx, app.canvas.canvas);
+
+ expect(group.node["images"]).toEqual([
+ {
+ filename: "test2.png",
+ type: "output",
+ },
+ ]);
+ });
+ test("allows widgets to be converted to inputs", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+ const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
+ group.widgets[0].convertToInput();
+
+ const primitive = ez.PrimitiveNode();
+ primitive.outputs[0].connectTo(group.inputs["text"]);
+ primitive.widgets[0].value = "hello";
+
+ expect((await graph.toPrompt()).output).toEqual(
+ getOutput([nodes.pos.id, nodes.neg.id], {
+ [nodes.pos.id]: { text: "hello" },
+ })
+ );
+ });
+ test("can be copied", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+
+ const group1 = await convertToGroup(app, graph, "test", [
+ nodes.pos,
+ nodes.neg,
+ nodes.empty,
+ nodes.sampler,
+ nodes.decode,
+ nodes.save,
+ ]);
+
+ group1.widgets["text"].value = "hello";
+ group1.widgets["width"].value = 256;
+ group1.widgets["seed"].value = 1;
+
+ // Clone the node
+ group1.menu.Clone.call();
+ expect(app.graph._nodes).toHaveLength(3);
+ const group2 = graph.find(app.graph._nodes[2]);
+ expect(group2.node.type).toEqual("workflow/test");
+ expect(group2.id).not.toEqual(group1.id);
+
+ // Reconnect ckpt
+ nodes.ckpt.outputs.MODEL.connectTo(group2.inputs["model"]);
+ nodes.ckpt.outputs.CLIP.connectTo(group2.inputs["clip"]);
+ nodes.ckpt.outputs.CLIP.connectTo(group2.inputs["CLIPTextEncode clip"]);
+ nodes.ckpt.outputs.VAE.connectTo(group2.inputs["vae"]);
+
+ group2.widgets["text"].value = "world";
+ group2.widgets["width"].value = 1024;
+ group2.widgets["seed"].value = 100;
+
+ let i = 0;
+ expect((await graph.toPrompt()).output).toEqual({
+ ...getOutput([nodes.empty.id, nodes.pos.id, nodes.neg.id, nodes.sampler.id, nodes.decode.id, nodes.save.id], {
+ [nodes.empty.id]: { width: 256 },
+ [nodes.pos.id]: { text: "hello" },
+ [nodes.sampler.id]: { seed: 1 },
+ }),
+ ...getOutput(
+ {
+ [nodes.empty.id]: `${group2.id}:${i++}`,
+ [nodes.pos.id]: `${group2.id}:${i++}`,
+ [nodes.neg.id]: `${group2.id}:${i++}`,
+ [nodes.sampler.id]: `${group2.id}:${i++}`,
+ [nodes.decode.id]: `${group2.id}:${i++}`,
+ [nodes.save.id]: `${group2.id}:${i++}`,
+ },
+ {
+ [nodes.empty.id]: { width: 1024 },
+ [nodes.pos.id]: { text: "world" },
+ [nodes.sampler.id]: { seed: 100 },
+ }
+ ),
+ });
+
+ graph.arrange();
+ });
+ test("is embedded in workflow", async () => {
+ let { ez, graph, app } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+ let group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
+ const workflow = JSON.stringify((await graph.toPrompt()).workflow);
+
+ // Clear the environment
+ ({ ez, graph, app } = await start({
+ resetEnv: true,
+ }));
+ // Ensure the node isnt registered
+ expect(() => ez["workflow/test"]).toThrow();
+
+ // Reload the workflow
+ await app.loadGraphData(JSON.parse(workflow));
+
+ // Ensure the node is found
+ group = graph.find(group);
+
+ // Generate prompt and ensure it is as expected
+ expect((await graph.toPrompt()).output).toEqual(
+ getOutput({
+ [nodes.pos.id]: `${group.id}:0`,
+ [nodes.neg.id]: `${group.id}:1`,
+ })
+ );
+ });
+ test("shows missing node error on missing internal node when loading graph data", async () => {
+ const { graph } = await start();
+
+ const dialogShow = jest.spyOn(graph.app.ui.dialog, "show");
+ await graph.app.loadGraphData({
+ last_node_id: 3,
+ last_link_id: 1,
+ nodes: [
+ {
+ id: 3,
+ type: "workflow/testerror",
+ },
+ ],
+ links: [],
+ groups: [],
+ config: {},
+ extra: {
+ groupNodes: {
+ testerror: {
+ nodes: [
+ {
+ type: "NotKSampler",
+ },
+ {
+ type: "NotVAEDecode",
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ expect(dialogShow).toBeCalledTimes(1);
+ const call = dialogShow.mock.calls[0][0].innerHTML;
+ expect(call).toContain("the following node types were not found");
+ expect(call).toContain("NotKSampler");
+ expect(call).toContain("NotVAEDecode");
+ expect(call).toContain("workflow/testerror");
+ });
+ test("maintains widget inputs on conversion back to nodes", async () => {
+ const { ez, graph, app } = await start();
+ let pos = ez.CLIPTextEncode({ text: "positive" });
+ pos.node.title = "Positive";
+ let neg = ez.CLIPTextEncode({ text: "negative" });
+ neg.node.title = "Negative";
+ pos.widgets.text.convertToInput();
+ neg.widgets.text.convertToInput();
+
+ let primitive = ez.PrimitiveNode();
+ primitive.outputs[0].connectTo(pos.inputs.text);
+ primitive.outputs[0].connectTo(neg.inputs.text);
+
+ const group = await convertToGroup(app, graph, "test", [pos, neg, primitive]);
+ // This will use a primitive widget named 'value'
+ expect(group.widgets.length).toBe(1);
+ expect(group.widgets["value"].value).toBe("positive");
+
+ const newNodes = group.menu["Convert to nodes"].call();
+ pos = graph.find(newNodes.find((n) => n.title === "Positive"));
+ neg = graph.find(newNodes.find((n) => n.title === "Negative"));
+ primitive = graph.find(newNodes.find((n) => n.type === "PrimitiveNode"));
+
+ expect(pos.inputs).toHaveLength(2);
+ expect(neg.inputs).toHaveLength(2);
+ expect(primitive.outputs[0].connections).toHaveLength(2);
+
+ expect((await graph.toPrompt()).output).toEqual({
+ 1: { inputs: { text: "positive" }, class_type: "CLIPTextEncode" },
+ 2: { inputs: { text: "positive" }, class_type: "CLIPTextEncode" },
+ });
+ });
+ test("correctly handles widget inputs", async () => {
+ const { ez, graph, app } = await start();
+ const upscaleMethods = (await getNodeDef("ImageScaleBy")).input.required["upscale_method"][0];
+
+ const image = ez.LoadImage();
+ const scale1 = ez.ImageScaleBy(image.outputs[0]);
+ const scale2 = ez.ImageScaleBy(image.outputs[0]);
+ const preview1 = ez.PreviewImage(scale1.outputs[0]);
+ const preview2 = ez.PreviewImage(scale2.outputs[0]);
+ scale1.widgets.upscale_method.value = upscaleMethods[1];
+ scale1.widgets.upscale_method.convertToInput();
+
+ const group = await convertToGroup(app, graph, "test", [scale1, scale2]);
+ expect(group.inputs.length).toBe(3);
+ expect(group.inputs[0].input.type).toBe("IMAGE");
+ expect(group.inputs[1].input.type).toBe("IMAGE");
+ expect(group.inputs[2].input.type).toBe("COMBO");
+
+ // Ensure links are maintained
+ expect(group.inputs[0].connection?.originNode?.id).toBe(image.id);
+ expect(group.inputs[1].connection?.originNode?.id).toBe(image.id);
+ expect(group.inputs[2].connection).toBeFalsy();
+
+ // Ensure primitive gets correct type
+ const primitive = ez.PrimitiveNode();
+ primitive.outputs[0].connectTo(group.inputs[2]);
+ expect(primitive.widgets.value.widget.options.values).toBe(upscaleMethods);
+ expect(primitive.widgets.value.value).toBe(upscaleMethods[1]); // Ensure value is copied
+ primitive.widgets.value.value = upscaleMethods[1];
+
+ await checkBeforeAndAfterReload(graph, async (r) => {
+ const scale1id = r ? `${group.id}:0` : scale1.id;
+ const scale2id = r ? `${group.id}:1` : scale2.id;
+ // Ensure widget value is applied to prompt
+ expect((await graph.toPrompt()).output).toStrictEqual({
+ [image.id]: { inputs: { image: "example.png", upload: "image" }, class_type: "LoadImage" },
+ [scale1id]: {
+ inputs: { upscale_method: upscaleMethods[1], scale_by: 1, image: [`${image.id}`, 0] },
+ class_type: "ImageScaleBy",
+ },
+ [scale2id]: {
+ inputs: { upscale_method: "nearest-exact", scale_by: 1, image: [`${image.id}`, 0] },
+ class_type: "ImageScaleBy",
+ },
+ [preview1.id]: { inputs: { images: [`${scale1id}`, 0] }, class_type: "PreviewImage" },
+ [preview2.id]: { inputs: { images: [`${scale2id}`, 0] }, class_type: "PreviewImage" },
+ });
+ });
+ });
+ test("adds widgets in node execution order", async () => {
+ const { ez, graph, app } = await start();
+ const scale = ez.LatentUpscale();
+ const save = ez.SaveImage();
+ const empty = ez.EmptyLatentImage();
+ const decode = ez.VAEDecode();
+
+ scale.outputs.LATENT.connectTo(decode.inputs.samples);
+ decode.outputs.IMAGE.connectTo(save.inputs.images);
+ empty.outputs.LATENT.connectTo(scale.inputs.samples);
+
+ const group = await convertToGroup(app, graph, "test", [scale, save, empty, decode]);
+ const widgets = group.widgets.map((w) => w.widget.name);
+ expect(widgets).toStrictEqual([
+ "width",
+ "height",
+ "batch_size",
+ "upscale_method",
+ "LatentUpscale width",
+ "LatentUpscale height",
+ "crop",
+ "filename_prefix",
+ ]);
+ });
+ test("adds output for external links when converting to group", async () => {
+ const { ez, graph, app } = await start();
+ const img = ez.EmptyLatentImage();
+ let decode = ez.VAEDecode(...img.outputs);
+ const preview1 = ez.PreviewImage(...decode.outputs);
+ const preview2 = ez.PreviewImage(...decode.outputs);
+
+ const group = await convertToGroup(app, graph, "test", [img, decode, preview1]);
+
+ // Ensure we have an output connected to the 2nd preview node
+ expect(group.outputs.length).toBe(1);
+ expect(group.outputs[0].connections.length).toBe(1);
+ expect(group.outputs[0].connections[0].targetNode.id).toBe(preview2.id);
+
+ // Convert back and ensure bothe previews are still connected
+ group.menu["Convert to nodes"].call();
+ decode = graph.find(decode);
+ expect(decode.outputs[0].connections.length).toBe(2);
+ expect(decode.outputs[0].connections[0].targetNode.id).toBe(preview1.id);
+ expect(decode.outputs[0].connections[1].targetNode.id).toBe(preview2.id);
+ });
+ test("adds output for external links when converting to group when nodes are not in execution order", async () => {
+ const { ez, graph, app } = await start();
+ const sampler = ez.KSampler();
+ const ckpt = ez.CheckpointLoaderSimple();
+ const empty = ez.EmptyLatentImage();
+ const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" });
+ const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" });
+ const decode1 = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE);
+ const save = ez.SaveImage(decode1.outputs.IMAGE);
+ ckpt.outputs.MODEL.connectTo(sampler.inputs.model);
+ pos.outputs.CONDITIONING.connectTo(sampler.inputs.positive);
+ neg.outputs.CONDITIONING.connectTo(sampler.inputs.negative);
+ empty.outputs.LATENT.connectTo(sampler.inputs.latent_image);
+
+ const encode = ez.VAEEncode(decode1.outputs.IMAGE);
+ const vae = ez.VAELoader();
+ const decode2 = ez.VAEDecode(encode.outputs.LATENT, vae.outputs.VAE);
+ const preview = ez.PreviewImage(decode2.outputs.IMAGE);
+ vae.outputs.VAE.connectTo(encode.inputs.vae);
+
+ const group = await convertToGroup(app, graph, "test", [vae, decode1, encode, sampler]);
+
+ expect(group.outputs.length).toBe(3);
+ expect(group.outputs[0].output.name).toBe("VAE");
+ expect(group.outputs[0].output.type).toBe("VAE");
+ expect(group.outputs[1].output.name).toBe("IMAGE");
+ expect(group.outputs[1].output.type).toBe("IMAGE");
+ expect(group.outputs[2].output.name).toBe("LATENT");
+ expect(group.outputs[2].output.type).toBe("LATENT");
+
+ expect(group.outputs[0].connections.length).toBe(1);
+ expect(group.outputs[0].connections[0].targetNode.id).toBe(decode2.id);
+ expect(group.outputs[0].connections[0].targetInput.index).toBe(1);
+
+ expect(group.outputs[1].connections.length).toBe(1);
+ expect(group.outputs[1].connections[0].targetNode.id).toBe(save.id);
+ expect(group.outputs[1].connections[0].targetInput.index).toBe(0);
+
+ expect(group.outputs[2].connections.length).toBe(1);
+ expect(group.outputs[2].connections[0].targetNode.id).toBe(decode2.id);
+ expect(group.outputs[2].connections[0].targetInput.index).toBe(0);
+
+ expect((await graph.toPrompt()).output).toEqual({
+ ...getOutput({ 1: ckpt.id, 2: pos.id, 3: neg.id, 4: empty.id, 5: sampler.id, 6: decode1.id, 7: save.id }),
+ [vae.id]: { inputs: { vae_name: "vae1.safetensors" }, class_type: vae.node.type },
+ [encode.id]: { inputs: { pixels: ["6", 0], vae: [vae.id + "", 0] }, class_type: encode.node.type },
+ [decode2.id]: { inputs: { samples: [encode.id + "", 0], vae: [vae.id + "", 0] }, class_type: decode2.node.type },
+ [preview.id]: { inputs: { images: [decode2.id + "", 0] }, class_type: preview.node.type },
+ });
+ });
+ test("works with IMAGEUPLOAD widget", async () => {
+ const { ez, graph, app } = await start();
+ const img = ez.LoadImage();
+ const preview1 = ez.PreviewImage(img.outputs[0]);
+
+ const group = await convertToGroup(app, graph, "test", [img, preview1]);
+ const widget = group.widgets["upload"];
+ expect(widget).toBeTruthy();
+ expect(widget.widget.type).toBe("button");
+ });
+ test("internal primitive populates widgets for all linked inputs", async () => {
+ const { ez, graph, app } = await start();
+ const img = ez.LoadImage();
+ const scale1 = ez.ImageScale(img.outputs[0]);
+ const scale2 = ez.ImageScale(img.outputs[0]);
+ ez.PreviewImage(scale1.outputs[0]);
+ ez.PreviewImage(scale2.outputs[0]);
+
+ scale1.widgets.width.convertToInput();
+ scale2.widgets.height.convertToInput();
+
+ const primitive = ez.PrimitiveNode();
+ primitive.outputs[0].connectTo(scale1.inputs.width);
+ primitive.outputs[0].connectTo(scale2.inputs.height);
+
+ const group = await convertToGroup(app, graph, "test", [img, primitive, scale1, scale2]);
+ group.widgets.value.value = 100;
+ expect((await graph.toPrompt()).output).toEqual({
+ 1: {
+ inputs: { image: img.widgets.image.value, upload: "image" },
+ class_type: "LoadImage",
+ },
+ 2: {
+ inputs: { upscale_method: "nearest-exact", width: 100, height: 512, crop: "disabled", image: ["1", 0] },
+ class_type: "ImageScale",
+ },
+ 3: {
+ inputs: { upscale_method: "nearest-exact", width: 512, height: 100, crop: "disabled", image: ["1", 0] },
+ class_type: "ImageScale",
+ },
+ 4: { inputs: { images: ["2", 0] }, class_type: "PreviewImage" },
+ 5: { inputs: { images: ["3", 0] }, class_type: "PreviewImage" },
+ });
+ });
+ test("primitive control widgets values are copied on convert", async () => {
+ const { ez, graph, app } = await start();
+ const sampler = ez.KSampler();
+ sampler.widgets.seed.convertToInput();
+ sampler.widgets.sampler_name.convertToInput();
+
+ let p1 = ez.PrimitiveNode();
+ let p2 = ez.PrimitiveNode();
+ p1.outputs[0].connectTo(sampler.inputs.seed);
+ p2.outputs[0].connectTo(sampler.inputs.sampler_name);
+
+ p1.widgets.control_after_generate.value = "increment";
+ p2.widgets.control_after_generate.value = "decrement";
+ p2.widgets.control_filter_list.value = "/.*/";
+
+ p2.node.title = "p2";
+
+ const group = await convertToGroup(app, graph, "test", [sampler, p1, p2]);
+ expect(group.widgets.control_after_generate.value).toBe("increment");
+ expect(group.widgets["p2 control_after_generate"].value).toBe("decrement");
+ expect(group.widgets["p2 control_filter_list"].value).toBe("/.*/");
+
+ group.widgets.control_after_generate.value = "fixed";
+ group.widgets["p2 control_after_generate"].value = "randomize";
+ group.widgets["p2 control_filter_list"].value = "/.+/";
+
+ group.menu["Convert to nodes"].call();
+ p1 = graph.find(p1);
+ p2 = graph.find(p2);
+
+ expect(p1.widgets.control_after_generate.value).toBe("fixed");
+ expect(p2.widgets.control_after_generate.value).toBe("randomize");
+ expect(p2.widgets.control_filter_list.value).toBe("/.+/");
+ });
+ test("internal reroutes work with converted inputs and merge options", async () => {
+ const { ez, graph, app } = await start();
+ const vae = ez.VAELoader();
+ const latent = ez.EmptyLatentImage();
+ const decode = ez.VAEDecode(latent.outputs.LATENT, vae.outputs.VAE);
+ const scale = ez.ImageScale(decode.outputs.IMAGE);
+ ez.PreviewImage(scale.outputs.IMAGE);
+
+ const r1 = ez.Reroute();
+ const r2 = ez.Reroute();
+
+ latent.widgets.width.value = 64;
+ latent.widgets.height.value = 128;
+
+ latent.widgets.width.convertToInput();
+ latent.widgets.height.convertToInput();
+ latent.widgets.batch_size.convertToInput();
+
+ scale.widgets.width.convertToInput();
+ scale.widgets.height.convertToInput();
+
+ r1.inputs[0].input.label = "hbw";
+ r1.outputs[0].connectTo(latent.inputs.height);
+ r1.outputs[0].connectTo(latent.inputs.batch_size);
+ r1.outputs[0].connectTo(scale.inputs.width);
+
+ r2.inputs[0].input.label = "wh";
+ r2.outputs[0].connectTo(latent.inputs.width);
+ r2.outputs[0].connectTo(scale.inputs.height);
+
+ const group = await convertToGroup(app, graph, "test", [r1, r2, latent, decode, scale]);
+
+ expect(group.inputs[0].input.type).toBe("VAE");
+ expect(group.inputs[1].input.type).toBe("INT");
+ expect(group.inputs[2].input.type).toBe("INT");
+
+ const p1 = ez.PrimitiveNode();
+ const p2 = ez.PrimitiveNode();
+ p1.outputs[0].connectTo(group.inputs[1]);
+ p2.outputs[0].connectTo(group.inputs[2]);
+
+ expect(p1.widgets.value.widget.options?.min).toBe(16); // width/height min
+ expect(p1.widgets.value.widget.options?.max).toBe(4096); // batch max
+ expect(p1.widgets.value.widget.options?.step).toBe(80); // width/height step * 10
+
+ expect(p2.widgets.value.widget.options?.min).toBe(16); // width/height min
+ expect(p2.widgets.value.widget.options?.max).toBe(8192); // width/height max
+ expect(p2.widgets.value.widget.options?.step).toBe(80); // width/height step * 10
+
+ expect(p1.widgets.value.value).toBe(128);
+ expect(p2.widgets.value.value).toBe(64);
+
+ p1.widgets.value.value = 16;
+ p2.widgets.value.value = 32;
+
+ await checkBeforeAndAfterReload(graph, async (r) => {
+ const id = (v) => (r ? `${group.id}:` : "") + v;
+ expect((await graph.toPrompt()).output).toStrictEqual({
+ 1: { inputs: { vae_name: "vae1.safetensors" }, class_type: "VAELoader" },
+ [id(2)]: { inputs: { width: 32, height: 16, batch_size: 16 }, class_type: "EmptyLatentImage" },
+ [id(3)]: { inputs: { samples: [id(2), 0], vae: ["1", 0] }, class_type: "VAEDecode" },
+ [id(4)]: {
+ inputs: { upscale_method: "nearest-exact", width: 16, height: 32, crop: "disabled", image: [id(3), 0] },
+ class_type: "ImageScale",
+ },
+ 5: { inputs: { images: [id(4), 0] }, class_type: "PreviewImage" },
+ });
+ });
+ });
+ test("converted inputs with linked widgets map values correctly on creation", async () => {
+ const { ez, graph, app } = await start();
+ const k1 = ez.KSampler();
+ const k2 = ez.KSampler();
+ k1.widgets.seed.convertToInput();
+ k2.widgets.seed.convertToInput();
+
+ const rr = ez.Reroute();
+ rr.outputs[0].connectTo(k1.inputs.seed);
+ rr.outputs[0].connectTo(k2.inputs.seed);
+
+ const group = await convertToGroup(app, graph, "test", [k1, k2, rr]);
+ expect(group.widgets.steps.value).toBe(20);
+ expect(group.widgets.cfg.value).toBe(8);
+ expect(group.widgets.scheduler.value).toBe("normal");
+ expect(group.widgets["KSampler steps"].value).toBe(20);
+ expect(group.widgets["KSampler cfg"].value).toBe(8);
+ expect(group.widgets["KSampler scheduler"].value).toBe("normal");
+ });
+ test("allow multiple of the same node type to be added", async () => {
+ const { ez, graph, app } = await start();
+ const nodes = [...Array(10)].map(() => ez.ImageScaleBy());
+ const group = await convertToGroup(app, graph, "test", nodes);
+ expect(group.inputs.length).toBe(10);
+ expect(group.outputs.length).toBe(10);
+ expect(group.widgets.length).toBe(20);
+ expect(group.widgets.map((w) => w.widget.name)).toStrictEqual(
+ [...Array(10)]
+ .map((_, i) => `${i > 0 ? "ImageScaleBy " : ""}${i > 1 ? i + " " : ""}`)
+ .flatMap((p) => [`${p}upscale_method`, `${p}scale_by`])
+ );
+ });
+});
diff --git a/ComfyUI/tests-ui/tests/widgetInputs.test.js b/ComfyUI/tests-ui/tests/widgetInputs.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..67e3fa341ecbfa3a9e331e74449f5cd264263b7a
--- /dev/null
+++ b/ComfyUI/tests-ui/tests/widgetInputs.test.js
@@ -0,0 +1,557 @@
+// @ts-check
+///
+
+const {
+ start,
+ makeNodeDef,
+ checkBeforeAndAfterReload,
+ assertNotNullOrUndefined,
+ createDefaultWorkflow,
+} = require("../utils");
+const lg = require("../utils/litegraph");
+
+/**
+ * @typedef { import("../utils/ezgraph") } Ez
+ * @typedef { ReturnType["ez"] } EzNodeFactory
+ */
+
+/**
+ * @param { EzNodeFactory } ez
+ * @param { InstanceType } graph
+ * @param { InstanceType } input
+ * @param { string } widgetType
+ * @param { number } controlWidgetCount
+ * @returns
+ */
+async function connectPrimitiveAndReload(ez, graph, input, widgetType, controlWidgetCount = 0) {
+ // Connect to primitive and ensure its still connected after
+ let primitive = ez.PrimitiveNode();
+ primitive.outputs[0].connectTo(input);
+
+ await checkBeforeAndAfterReload(graph, async () => {
+ primitive = graph.find(primitive);
+ let { connections } = primitive.outputs[0];
+ expect(connections).toHaveLength(1);
+ expect(connections[0].targetNode.id).toBe(input.node.node.id);
+
+ // Ensure widget is correct type
+ const valueWidget = primitive.widgets.value;
+ expect(valueWidget.widget.type).toBe(widgetType);
+
+ // Check if control_after_generate should be added
+ if (controlWidgetCount) {
+ const controlWidget = primitive.widgets.control_after_generate;
+ expect(controlWidget.widget.type).toBe("combo");
+ if (widgetType === "combo") {
+ const filterWidget = primitive.widgets.control_filter_list;
+ expect(filterWidget.widget.type).toBe("string");
+ }
+ }
+
+ // Ensure we dont have other widgets
+ expect(primitive.node.widgets).toHaveLength(1 + controlWidgetCount);
+ });
+
+ return primitive;
+}
+
+describe("widget inputs", () => {
+ beforeEach(() => {
+ lg.setup(global);
+ });
+
+ afterEach(() => {
+ lg.teardown(global);
+ });
+
+ [
+ { name: "int", type: "INT", widget: "number", control: 1 },
+ { name: "float", type: "FLOAT", widget: "number", control: 1 },
+ { name: "text", type: "STRING" },
+ {
+ name: "customtext",
+ type: "STRING",
+ opt: { multiline: true },
+ },
+ { name: "toggle", type: "BOOLEAN" },
+ { name: "combo", type: ["a", "b", "c"], control: 2 },
+ ].forEach((c) => {
+ test(`widget conversion + primitive works on ${c.name}`, async () => {
+ const { ez, graph } = await start({
+ mockNodeDefs: makeNodeDef("TestNode", { [c.name]: [c.type, c.opt ?? {}] }),
+ });
+
+ // Create test node and convert to input
+ const n = ez.TestNode();
+ const w = n.widgets[c.name];
+ w.convertToInput();
+ expect(w.isConvertedToInput).toBeTruthy();
+ const input = w.getConvertedInput();
+ expect(input).toBeTruthy();
+
+ // @ts-ignore : input is valid here
+ await connectPrimitiveAndReload(ez, graph, input, c.widget ?? c.name, c.control);
+ });
+ });
+
+ test("converted widget works after reload", async () => {
+ const { ez, graph } = await start();
+ let n = ez.CheckpointLoaderSimple();
+
+ const inputCount = n.inputs.length;
+
+ // Convert ckpt name to an input
+ n.widgets.ckpt_name.convertToInput();
+ expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
+ expect(n.inputs.ckpt_name).toBeTruthy();
+ expect(n.inputs.length).toEqual(inputCount + 1);
+
+ // Convert back to widget and ensure input is removed
+ n.widgets.ckpt_name.convertToWidget();
+ expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
+ expect(n.inputs.ckpt_name).toBeFalsy();
+ expect(n.inputs.length).toEqual(inputCount);
+
+ // Convert again and reload the graph to ensure it maintains state
+ n.widgets.ckpt_name.convertToInput();
+ expect(n.inputs.length).toEqual(inputCount + 1);
+
+ const primitive = await connectPrimitiveAndReload(ez, graph, n.inputs.ckpt_name, "combo", 2);
+
+ // Disconnect & reconnect
+ primitive.outputs[0].connections[0].disconnect();
+ let { connections } = primitive.outputs[0];
+ expect(connections).toHaveLength(0);
+
+ primitive.outputs[0].connectTo(n.inputs.ckpt_name);
+ ({ connections } = primitive.outputs[0]);
+ expect(connections).toHaveLength(1);
+ expect(connections[0].targetNode.id).toBe(n.node.id);
+
+ // Convert back to widget and ensure input is removed
+ n.widgets.ckpt_name.convertToWidget();
+ expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
+ expect(n.inputs.ckpt_name).toBeFalsy();
+ expect(n.inputs.length).toEqual(inputCount);
+ });
+
+ test("converted widget works on clone", async () => {
+ const { graph, ez } = await start();
+ let n = ez.CheckpointLoaderSimple();
+
+ // Convert the widget to an input
+ n.widgets.ckpt_name.convertToInput();
+ expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
+
+ // Clone the node
+ n.menu["Clone"].call();
+ expect(graph.nodes).toHaveLength(2);
+ const clone = graph.nodes[1];
+ expect(clone.id).not.toEqual(n.id);
+
+ // Ensure the clone has an input
+ expect(clone.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
+ expect(clone.inputs.ckpt_name).toBeTruthy();
+
+ // Ensure primitive connects to both nodes
+ let primitive = ez.PrimitiveNode();
+ primitive.outputs[0].connectTo(n.inputs.ckpt_name);
+ primitive.outputs[0].connectTo(clone.inputs.ckpt_name);
+ expect(primitive.outputs[0].connections).toHaveLength(2);
+
+ // Convert back to widget and ensure input is removed
+ clone.widgets.ckpt_name.convertToWidget();
+ expect(clone.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
+ expect(clone.inputs.ckpt_name).toBeFalsy();
+ });
+
+ test("shows missing node error on custom node with converted input", async () => {
+ const { graph } = await start();
+
+ const dialogShow = jest.spyOn(graph.app.ui.dialog, "show");
+
+ await graph.app.loadGraphData({
+ last_node_id: 3,
+ last_link_id: 4,
+ nodes: [
+ {
+ id: 1,
+ type: "TestNode",
+ pos: [41.87329101561909, 389.7381480823742],
+ size: { 0: 220, 1: 374 },
+ flags: {},
+ order: 1,
+ mode: 0,
+ inputs: [{ name: "test", type: "FLOAT", link: 4, widget: { name: "test" }, slot_index: 0 }],
+ outputs: [],
+ properties: { "Node name for S&R": "TestNode" },
+ widgets_values: [1],
+ },
+ {
+ id: 3,
+ type: "PrimitiveNode",
+ pos: [-312, 433],
+ size: { 0: 210, 1: 82 },
+ flags: {},
+ order: 0,
+ mode: 0,
+ outputs: [{ links: [4], widget: { name: "test" } }],
+ title: "test",
+ properties: {},
+ },
+ ],
+ links: [[4, 3, 0, 1, 6, "FLOAT"]],
+ groups: [],
+ config: {},
+ extra: {},
+ version: 0.4,
+ });
+
+ expect(dialogShow).toBeCalledTimes(1);
+ expect(dialogShow.mock.calls[0][0].innerHTML).toContain("the following node types were not found");
+ expect(dialogShow.mock.calls[0][0].innerHTML).toContain("TestNode");
+ });
+
+ test("defaultInput widgets can be converted back to inputs", async () => {
+ const { graph, ez } = await start({
+ mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { defaultInput: true }] }),
+ });
+
+ // Create test node and ensure it starts as an input
+ let n = ez.TestNode();
+ let w = n.widgets.example;
+ expect(w.isConvertedToInput).toBeTruthy();
+ let input = w.getConvertedInput();
+ expect(input).toBeTruthy();
+
+ // Ensure it can be converted to
+ w.convertToWidget();
+ expect(w.isConvertedToInput).toBeFalsy();
+ expect(n.inputs.length).toEqual(0);
+ // and from
+ w.convertToInput();
+ expect(w.isConvertedToInput).toBeTruthy();
+ input = w.getConvertedInput();
+
+ // Reload and ensure it still only has 1 converted widget
+ if (!assertNotNullOrUndefined(input)) return;
+
+ await connectPrimitiveAndReload(ez, graph, input, "number", 1);
+ n = graph.find(n);
+ expect(n.widgets).toHaveLength(1);
+ w = n.widgets.example;
+ expect(w.isConvertedToInput).toBeTruthy();
+
+ // Convert back to widget and ensure it is still a widget after reload
+ w.convertToWidget();
+ await graph.reload();
+ n = graph.find(n);
+ expect(n.widgets).toHaveLength(1);
+ expect(n.widgets[0].isConvertedToInput).toBeFalsy();
+ expect(n.inputs.length).toEqual(0);
+ });
+
+ test("forceInput widgets can not be converted back to inputs", async () => {
+ const { graph, ez } = await start({
+ mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { forceInput: true }] }),
+ });
+
+ // Create test node and ensure it starts as an input
+ let n = ez.TestNode();
+ let w = n.widgets.example;
+ expect(w.isConvertedToInput).toBeTruthy();
+ const input = w.getConvertedInput();
+ expect(input).toBeTruthy();
+
+ // Convert to widget should error
+ expect(() => w.convertToWidget()).toThrow();
+
+ // Reload and ensure it still only has 1 converted widget
+ if (assertNotNullOrUndefined(input)) {
+ await connectPrimitiveAndReload(ez, graph, input, "number", 1);
+ n = graph.find(n);
+ expect(n.widgets).toHaveLength(1);
+ expect(n.widgets.example.isConvertedToInput).toBeTruthy();
+ }
+ });
+
+ test("primitive can connect to matching combos on converted widgets", async () => {
+ const { ez } = await start({
+ mockNodeDefs: {
+ ...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }),
+ ...makeNodeDef("TestNode2", { example: [["A", "B", "C"], { forceInput: true }] }),
+ },
+ });
+
+ const n1 = ez.TestNode1();
+ const n2 = ez.TestNode2();
+ const p = ez.PrimitiveNode();
+ p.outputs[0].connectTo(n1.inputs[0]);
+ p.outputs[0].connectTo(n2.inputs[0]);
+ expect(p.outputs[0].connections).toHaveLength(2);
+ const valueWidget = p.widgets.value;
+ expect(valueWidget.widget.type).toBe("combo");
+ expect(valueWidget.widget.options.values).toEqual(["A", "B", "C"]);
+ });
+
+ test("primitive can not connect to non matching combos on converted widgets", async () => {
+ const { ez } = await start({
+ mockNodeDefs: {
+ ...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }),
+ ...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }),
+ },
+ });
+
+ const n1 = ez.TestNode1();
+ const n2 = ez.TestNode2();
+ const p = ez.PrimitiveNode();
+ p.outputs[0].connectTo(n1.inputs[0]);
+ expect(() => p.outputs[0].connectTo(n2.inputs[0])).toThrow();
+ expect(p.outputs[0].connections).toHaveLength(1);
+ });
+
+ test("combo output can not connect to non matching combos list input", async () => {
+ const { ez } = await start({
+ mockNodeDefs: {
+ ...makeNodeDef("TestNode1", {}, [["A", "B"]]),
+ ...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }),
+ ...makeNodeDef("TestNode3", { example: [["A", "B", "C"], { forceInput: true }] }),
+ },
+ });
+
+ const n1 = ez.TestNode1();
+ const n2 = ez.TestNode2();
+ const n3 = ez.TestNode3();
+
+ n1.outputs[0].connectTo(n2.inputs[0]);
+ expect(() => n1.outputs[0].connectTo(n3.inputs[0])).toThrow();
+ });
+
+ test("combo primitive can filter list when control_after_generate called", async () => {
+ const { ez } = await start({
+ mockNodeDefs: {
+ ...makeNodeDef("TestNode1", { example: [["A", "B", "C", "D", "AA", "BB", "CC", "DD", "AAA", "BBB"], {}] }),
+ },
+ });
+
+ const n1 = ez.TestNode1();
+ n1.widgets.example.convertToInput();
+ const p = ez.PrimitiveNode();
+ p.outputs[0].connectTo(n1.inputs[0]);
+
+ const value = p.widgets.value;
+ const control = p.widgets.control_after_generate.widget;
+ const filter = p.widgets.control_filter_list;
+
+ expect(p.widgets.length).toBe(3);
+ control.value = "increment";
+ expect(value.value).toBe("A");
+
+ // Manually trigger after queue when set to increment
+ control["afterQueued"]();
+ expect(value.value).toBe("B");
+
+ // Filter to items containing D
+ filter.value = "D";
+ control["afterQueued"]();
+ expect(value.value).toBe("D");
+ control["afterQueued"]();
+ expect(value.value).toBe("DD");
+
+ // Check decrement
+ value.value = "BBB";
+ control.value = "decrement";
+ filter.value = "B";
+ control["afterQueued"]();
+ expect(value.value).toBe("BB");
+ control["afterQueued"]();
+ expect(value.value).toBe("B");
+
+ // Check regex works
+ value.value = "BBB";
+ filter.value = "/[AB]|^C$/";
+ control["afterQueued"]();
+ expect(value.value).toBe("AAA");
+ control["afterQueued"]();
+ expect(value.value).toBe("BB");
+ control["afterQueued"]();
+ expect(value.value).toBe("AA");
+ control["afterQueued"]();
+ expect(value.value).toBe("C");
+ control["afterQueued"]();
+ expect(value.value).toBe("B");
+ control["afterQueued"]();
+ expect(value.value).toBe("A");
+
+ // Check random
+ control.value = "randomize";
+ filter.value = "/D/";
+ for (let i = 0; i < 100; i++) {
+ control["afterQueued"]();
+ expect(value.value === "D" || value.value === "DD").toBeTruthy();
+ }
+
+ // Ensure it doesnt apply when fixed
+ control.value = "fixed";
+ value.value = "B";
+ filter.value = "C";
+ control["afterQueued"]();
+ expect(value.value).toBe("B");
+ });
+
+ describe("reroutes", () => {
+ async function checkOutput(graph, values) {
+ expect((await graph.toPrompt()).output).toStrictEqual({
+ 1: { inputs: { ckpt_name: "model1.safetensors" }, class_type: "CheckpointLoaderSimple" },
+ 2: { inputs: { text: "positive", clip: ["1", 1] }, class_type: "CLIPTextEncode" },
+ 3: { inputs: { text: "negative", clip: ["1", 1] }, class_type: "CLIPTextEncode" },
+ 4: {
+ inputs: { width: values.width ?? 512, height: values.height ?? 512, batch_size: values?.batch_size ?? 1 },
+ class_type: "EmptyLatentImage",
+ },
+ 5: {
+ inputs: {
+ seed: 0,
+ steps: 20,
+ cfg: 8,
+ sampler_name: "euler",
+ scheduler: values?.scheduler ?? "normal",
+ denoise: 1,
+ model: ["1", 0],
+ positive: ["2", 0],
+ negative: ["3", 0],
+ latent_image: ["4", 0],
+ },
+ class_type: "KSampler",
+ },
+ 6: { inputs: { samples: ["5", 0], vae: ["1", 2] }, class_type: "VAEDecode" },
+ 7: {
+ inputs: { filename_prefix: values.filename_prefix ?? "ComfyUI", images: ["6", 0] },
+ class_type: "SaveImage",
+ },
+ });
+ }
+
+ async function waitForWidget(node) {
+ // widgets are created slightly after the graph is ready
+ // hard to find an exact hook to get these so just wait for them to be ready
+ for (let i = 0; i < 10; i++) {
+ await new Promise((r) => setTimeout(r, 10));
+ if (node.widgets?.value) {
+ return;
+ }
+ }
+ }
+
+ it("can connect primitive via a reroute path to a widget input", async () => {
+ const { ez, graph } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+
+ nodes.empty.widgets.width.convertToInput();
+ nodes.sampler.widgets.scheduler.convertToInput();
+ nodes.save.widgets.filename_prefix.convertToInput();
+
+ let widthReroute = ez.Reroute();
+ let schedulerReroute = ez.Reroute();
+ let fileReroute = ez.Reroute();
+
+ let widthNext = widthReroute;
+ let schedulerNext = schedulerReroute;
+ let fileNext = fileReroute;
+
+ for (let i = 0; i < 5; i++) {
+ let next = ez.Reroute();
+ widthNext.outputs[0].connectTo(next.inputs[0]);
+ widthNext = next;
+
+ next = ez.Reroute();
+ schedulerNext.outputs[0].connectTo(next.inputs[0]);
+ schedulerNext = next;
+
+ next = ez.Reroute();
+ fileNext.outputs[0].connectTo(next.inputs[0]);
+ fileNext = next;
+ }
+
+ widthNext.outputs[0].connectTo(nodes.empty.inputs.width);
+ schedulerNext.outputs[0].connectTo(nodes.sampler.inputs.scheduler);
+ fileNext.outputs[0].connectTo(nodes.save.inputs.filename_prefix);
+
+ let widthPrimitive = ez.PrimitiveNode();
+ let schedulerPrimitive = ez.PrimitiveNode();
+ let filePrimitive = ez.PrimitiveNode();
+
+ widthPrimitive.outputs[0].connectTo(widthReroute.inputs[0]);
+ schedulerPrimitive.outputs[0].connectTo(schedulerReroute.inputs[0]);
+ filePrimitive.outputs[0].connectTo(fileReroute.inputs[0]);
+ expect(widthPrimitive.widgets.value.value).toBe(512);
+ widthPrimitive.widgets.value.value = 1024;
+ expect(schedulerPrimitive.widgets.value.value).toBe("normal");
+ schedulerPrimitive.widgets.value.value = "simple";
+ expect(filePrimitive.widgets.value.value).toBe("ComfyUI");
+ filePrimitive.widgets.value.value = "ComfyTest";
+
+ await checkBeforeAndAfterReload(graph, async () => {
+ widthPrimitive = graph.find(widthPrimitive);
+ schedulerPrimitive = graph.find(schedulerPrimitive);
+ filePrimitive = graph.find(filePrimitive);
+ await waitForWidget(filePrimitive);
+ expect(widthPrimitive.widgets.length).toBe(2);
+ expect(schedulerPrimitive.widgets.length).toBe(3);
+ expect(filePrimitive.widgets.length).toBe(1);
+
+ await checkOutput(graph, {
+ width: 1024,
+ scheduler: "simple",
+ filename_prefix: "ComfyTest",
+ });
+ });
+ });
+ it("can connect primitive via a reroute path to multiple widget inputs", async () => {
+ const { ez, graph } = await start();
+ const nodes = createDefaultWorkflow(ez, graph);
+
+ nodes.empty.widgets.width.convertToInput();
+ nodes.empty.widgets.height.convertToInput();
+ nodes.empty.widgets.batch_size.convertToInput();
+
+ let reroute = ez.Reroute();
+ let prevReroute = reroute;
+ for (let i = 0; i < 5; i++) {
+ const next = ez.Reroute();
+ prevReroute.outputs[0].connectTo(next.inputs[0]);
+ prevReroute = next;
+ }
+
+ const r1 = ez.Reroute(prevReroute.outputs[0]);
+ const r2 = ez.Reroute(prevReroute.outputs[0]);
+ const r3 = ez.Reroute(r2.outputs[0]);
+ const r4 = ez.Reroute(r2.outputs[0]);
+
+ r1.outputs[0].connectTo(nodes.empty.inputs.width);
+ r3.outputs[0].connectTo(nodes.empty.inputs.height);
+ r4.outputs[0].connectTo(nodes.empty.inputs.batch_size);
+
+ let primitive = ez.PrimitiveNode();
+ primitive.outputs[0].connectTo(reroute.inputs[0]);
+ expect(primitive.widgets.value.value).toBe(1);
+ primitive.widgets.value.value = 64;
+
+ await checkBeforeAndAfterReload(graph, async (r) => {
+ primitive = graph.find(primitive);
+ await waitForWidget(primitive);
+
+ // Ensure widget configs are merged
+ expect(primitive.widgets.value.widget.options?.min).toBe(16); // width/height min
+ expect(primitive.widgets.value.widget.options?.max).toBe(4096); // batch max
+ expect(primitive.widgets.value.widget.options?.step).toBe(80); // width/height step * 10
+
+ await checkOutput(graph, {
+ width: 64,
+ height: 64,
+ batch_size: 64,
+ });
+ });
+ });
+ });
+});
diff --git a/ComfyUI/tests-ui/utils/ezgraph.js b/ComfyUI/tests-ui/utils/ezgraph.js
new file mode 100644
index 0000000000000000000000000000000000000000..8a55246ee3d2ca5cfa2cef1c0b6e4cd913feb61c
--- /dev/null
+++ b/ComfyUI/tests-ui/utils/ezgraph.js
@@ -0,0 +1,448 @@
+// @ts-check
+///
+
+/**
+ * @typedef { import("../../web/scripts/app")["app"] } app
+ * @typedef { import("../../web/types/litegraph") } LG
+ * @typedef { import("../../web/types/litegraph").IWidget } IWidget
+ * @typedef { import("../../web/types/litegraph").ContextMenuItem } ContextMenuItem
+ * @typedef { import("../../web/types/litegraph").INodeInputSlot } INodeInputSlot
+ * @typedef { import("../../web/types/litegraph").INodeOutputSlot } INodeOutputSlot
+ * @typedef { InstanceType & { widgets?: Array } } LGNode
+ * @typedef { (...args: EzOutput[] | [...EzOutput[], Record]) => EzNode } EzNodeFactory
+ */
+
+export class EzConnection {
+ /** @type { app } */
+ app;
+ /** @type { InstanceType } */
+ link;
+
+ get originNode() {
+ return new EzNode(this.app, this.app.graph.getNodeById(this.link.origin_id));
+ }
+
+ get originOutput() {
+ return this.originNode.outputs[this.link.origin_slot];
+ }
+
+ get targetNode() {
+ return new EzNode(this.app, this.app.graph.getNodeById(this.link.target_id));
+ }
+
+ get targetInput() {
+ return this.targetNode.inputs[this.link.target_slot];
+ }
+
+ /**
+ * @param { app } app
+ * @param { InstanceType } link
+ */
+ constructor(app, link) {
+ this.app = app;
+ this.link = link;
+ }
+
+ disconnect() {
+ this.targetInput.disconnect();
+ }
+}
+
+export class EzSlot {
+ /** @type { EzNode } */
+ node;
+ /** @type { number } */
+ index;
+
+ /**
+ * @param { EzNode } node
+ * @param { number } index
+ */
+ constructor(node, index) {
+ this.node = node;
+ this.index = index;
+ }
+}
+
+export class EzInput extends EzSlot {
+ /** @type { INodeInputSlot } */
+ input;
+
+ /**
+ * @param { EzNode } node
+ * @param { number } index
+ * @param { INodeInputSlot } input
+ */
+ constructor(node, index, input) {
+ super(node, index);
+ this.input = input;
+ }
+
+ get connection() {
+ const link = this.node.node.inputs?.[this.index]?.link;
+ if (link == null) {
+ return null;
+ }
+ return new EzConnection(this.node.app, this.node.app.graph.links[link]);
+ }
+
+ disconnect() {
+ this.node.node.disconnectInput(this.index);
+ }
+}
+
+export class EzOutput extends EzSlot {
+ /** @type { INodeOutputSlot } */
+ output;
+
+ /**
+ * @param { EzNode } node
+ * @param { number } index
+ * @param { INodeOutputSlot } output
+ */
+ constructor(node, index, output) {
+ super(node, index);
+ this.output = output;
+ }
+
+ get connections() {
+ return (this.node.node.outputs?.[this.index]?.links ?? []).map(
+ (l) => new EzConnection(this.node.app, this.node.app.graph.links[l])
+ );
+ }
+
+ /**
+ * @param { EzInput } input
+ */
+ connectTo(input) {
+ if (!input) throw new Error("Invalid input");
+
+ /**
+ * @type { LG["LLink"] | null }
+ */
+ const link = this.node.node.connect(this.index, input.node.node, input.index);
+ if (!link) {
+ const inp = input.input;
+ const inName = inp.name || inp.label || inp.type;
+ throw new Error(
+ `Connecting from ${input.node.node.type}#${input.node.id}[${inName}#${input.index}] -> ${this.node.node.type}#${this.node.id}[${
+ this.output.name ?? this.output.type
+ }#${this.index}] failed.`
+ );
+ }
+ return link;
+ }
+}
+
+export class EzNodeMenuItem {
+ /** @type { EzNode } */
+ node;
+ /** @type { number } */
+ index;
+ /** @type { ContextMenuItem } */
+ item;
+
+ /**
+ * @param { EzNode } node
+ * @param { number } index
+ * @param { ContextMenuItem } item
+ */
+ constructor(node, index, item) {
+ this.node = node;
+ this.index = index;
+ this.item = item;
+ }
+
+ call(selectNode = true) {
+ if (!this.item?.callback) throw new Error(`Menu Item ${this.item?.content ?? "[null]"} has no callback.`);
+ if (selectNode) {
+ this.node.select();
+ }
+ return this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node);
+ }
+}
+
+export class EzWidget {
+ /** @type { EzNode } */
+ node;
+ /** @type { number } */
+ index;
+ /** @type { IWidget } */
+ widget;
+
+ /**
+ * @param { EzNode } node
+ * @param { number } index
+ * @param { IWidget } widget
+ */
+ constructor(node, index, widget) {
+ this.node = node;
+ this.index = index;
+ this.widget = widget;
+ }
+
+ get value() {
+ return this.widget.value;
+ }
+
+ set value(v) {
+ this.widget.value = v;
+ this.widget.callback?.call?.(this.widget, v)
+ }
+
+ get isConvertedToInput() {
+ // @ts-ignore : this type is valid for converted widgets
+ return this.widget.type === "converted-widget";
+ }
+
+ getConvertedInput() {
+ if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`);
+
+ return this.node.inputs.find((inp) => inp.input["widget"]?.name === this.widget.name);
+ }
+
+ convertToWidget() {
+ if (!this.isConvertedToInput)
+ throw new Error(`Widget ${this.widget.name} cannot be converted as it is already a widget.`);
+ this.node.menu[`Convert ${this.widget.name} to widget`].call();
+ }
+
+ convertToInput() {
+ if (this.isConvertedToInput)
+ throw new Error(`Widget ${this.widget.name} cannot be converted as it is already an input.`);
+ this.node.menu[`Convert ${this.widget.name} to input`].call();
+ }
+}
+
+export class EzNode {
+ /** @type { app } */
+ app;
+ /** @type { LGNode } */
+ node;
+
+ /**
+ * @param { app } app
+ * @param { LGNode } node
+ */
+ constructor(app, node) {
+ this.app = app;
+ this.node = node;
+ }
+
+ get id() {
+ return this.node.id;
+ }
+
+ get inputs() {
+ return this.#makeLookupArray("inputs", "name", EzInput);
+ }
+
+ get outputs() {
+ return this.#makeLookupArray("outputs", "name", EzOutput);
+ }
+
+ get widgets() {
+ return this.#makeLookupArray("widgets", "name", EzWidget);
+ }
+
+ get menu() {
+ return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
+ }
+
+ get isRemoved() {
+ return !this.app.graph.getNodeById(this.id);
+ }
+
+ select(addToSelection = false) {
+ this.app.canvas.selectNode(this.node, addToSelection);
+ }
+
+ // /**
+ // * @template { "inputs" | "outputs" } T
+ // * @param { T } type
+ // * @returns { Record & (type extends "inputs" ? EzInput [] : EzOutput[]) }
+ // */
+ // #getSlotItems(type) {
+ // // @ts-ignore : these items are correct
+ // return (this.node[type] ?? []).reduce((p, s, i) => {
+ // if (s.name in p) {
+ // throw new Error(`Unable to store input ${s.name} on array as name conflicts.`);
+ // }
+ // // @ts-ignore
+ // p.push((p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s)));
+ // return p;
+ // }, Object.assign([], { $: this }));
+ // }
+
+ /**
+ * @template { { new(node: EzNode, index: number, obj: any): any } } T
+ * @param { "inputs" | "outputs" | "widgets" | (() => Array) } nodeProperty
+ * @param { string } nameProperty
+ * @param { T } ctor
+ * @returns { Record> & Array> }
+ */
+ #makeLookupArray(nodeProperty, nameProperty, ctor) {
+ const items = typeof nodeProperty === "function" ? nodeProperty() : this.node[nodeProperty];
+ // @ts-ignore
+ return (items ?? []).reduce((p, s, i) => {
+ if (!s) return p;
+
+ const name = s[nameProperty];
+ const item = new ctor(this, i, s);
+ // @ts-ignore
+ p.push(item);
+ if (name) {
+ // @ts-ignore
+ if (name in p) {
+ throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`);
+ }
+ }
+ // @ts-ignore
+ p[name] = item;
+ return p;
+ }, Object.assign([], { $: this }));
+ }
+}
+
+export class EzGraph {
+ /** @type { app } */
+ app;
+
+ /**
+ * @param { app } app
+ */
+ constructor(app) {
+ this.app = app;
+ }
+
+ get nodes() {
+ return this.app.graph._nodes.map((n) => new EzNode(this.app, n));
+ }
+
+ clear() {
+ this.app.graph.clear();
+ }
+
+ arrange() {
+ this.app.graph.arrange();
+ }
+
+ stringify() {
+ return JSON.stringify(this.app.graph.serialize(), undefined);
+ }
+
+ /**
+ * @param { number | LGNode | EzNode } obj
+ * @returns { EzNode }
+ */
+ find(obj) {
+ let match;
+ let id;
+ if (typeof obj === "number") {
+ id = obj;
+ } else {
+ id = obj.id;
+ }
+
+ match = this.app.graph.getNodeById(id);
+
+ if (!match) {
+ throw new Error(`Unable to find node with ID ${id}.`);
+ }
+
+ return new EzNode(this.app, match);
+ }
+
+ /**
+ * @returns { Promise }
+ */
+ reload() {
+ const graph = JSON.parse(JSON.stringify(this.app.graph.serialize()));
+ return new Promise((r) => {
+ this.app.graph.clear();
+ setTimeout(async () => {
+ await this.app.loadGraphData(graph);
+ r();
+ }, 10);
+ });
+ }
+
+ /**
+ * @returns { Promise<{
+ * workflow: {},
+ * output: Record
+ * }>}> }
+ */
+ toPrompt() {
+ // @ts-ignore
+ return this.app.graphToPrompt();
+ }
+}
+
+export const Ez = {
+ /**
+ * Quickly build and interact with a ComfyUI graph
+ * @example
+ * const { ez, graph } = Ez.graph(app);
+ * graph.clear();
+ * const [model, clip, vae] = ez.CheckpointLoaderSimple().outputs;
+ * const [pos] = ez.CLIPTextEncode(clip, { text: "positive" }).outputs;
+ * const [neg] = ez.CLIPTextEncode(clip, { text: "negative" }).outputs;
+ * const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage().outputs).outputs;
+ * const [image] = ez.VAEDecode(latent, vae).outputs;
+ * const saveNode = ez.SaveImage(image);
+ * console.log(saveNode);
+ * graph.arrange();
+ * @param { app } app
+ * @param { LG["LiteGraph"] } LiteGraph
+ * @param { LG["LGraphCanvas"] } LGraphCanvas
+ * @param { boolean } clearGraph
+ * @returns { { graph: EzGraph, ez: Record } }
+ */
+ graph(app, LiteGraph = window["LiteGraph"], LGraphCanvas = window["LGraphCanvas"], clearGraph = true) {
+ // Always set the active canvas so things work
+ LGraphCanvas.active_canvas = app.canvas;
+
+ if (clearGraph) {
+ app.graph.clear();
+ }
+
+ // @ts-ignore : this proxy handles utility methods & node creation
+ const factory = new Proxy(
+ {},
+ {
+ get(_, p) {
+ if (typeof p !== "string") throw new Error("Invalid node");
+ const node = LiteGraph.createNode(p);
+ if (!node) throw new Error(`Unknown node "${p}"`);
+ app.graph.add(node);
+
+ /**
+ * @param {Parameters} args
+ */
+ return function (...args) {
+ const ezNode = new EzNode(app, node);
+ const inputs = ezNode.inputs;
+
+ let slot = 0;
+ for (const arg of args) {
+ if (arg instanceof EzOutput) {
+ arg.connectTo(inputs[slot++]);
+ } else {
+ for (const k in arg) {
+ ezNode.widgets[k].value = arg[k];
+ }
+ }
+ }
+
+ return ezNode;
+ };
+ },
+ }
+ );
+
+ return { graph: new EzGraph(app), ez: factory };
+ },
+};
diff --git a/ComfyUI/tests-ui/utils/index.js b/ComfyUI/tests-ui/utils/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..6a08e8594e9c8d80c3b149241c3daf105681465b
--- /dev/null
+++ b/ComfyUI/tests-ui/utils/index.js
@@ -0,0 +1,115 @@
+const { mockApi } = require("./setup");
+const { Ez } = require("./ezgraph");
+const lg = require("./litegraph");
+
+/**
+ *
+ * @param { Parameters[0] & { resetEnv?: boolean, preSetup?(app): Promise } } config
+ * @returns
+ */
+export async function start(config = {}) {
+ if(config.resetEnv) {
+ jest.resetModules();
+ jest.resetAllMocks();
+ lg.setup(global);
+ }
+
+ mockApi(config);
+ const { app } = require("../../web/scripts/app");
+ config.preSetup?.(app);
+ await app.setup();
+ return { ...Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]), app };
+}
+
+/**
+ * @param { ReturnType["graph"] } graph
+ * @param { (hasReloaded: boolean) => (Promise | void) } cb
+ */
+export async function checkBeforeAndAfterReload(graph, cb) {
+ await cb(false);
+ await graph.reload();
+ await cb(true);
+}
+
+/**
+ * @param { string } name
+ * @param { Record } input
+ * @param { (string | string[])[] | Record } output
+ * @returns { Record }
+ */
+export function makeNodeDef(name, input, output = {}) {
+ const nodeDef = {
+ name,
+ category: "test",
+ output: [],
+ output_name: [],
+ output_is_list: [],
+ input: {
+ required: {},
+ },
+ };
+ for (const k in input) {
+ nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]];
+ }
+ if (output instanceof Array) {
+ output = output.reduce((p, c) => {
+ p[c] = c;
+ return p;
+ }, {});
+ }
+ for (const k in output) {
+ nodeDef.output.push(output[k]);
+ nodeDef.output_name.push(k);
+ nodeDef.output_is_list.push(false);
+ }
+
+ return { [name]: nodeDef };
+}
+
+/**
+/**
+ * @template { any } T
+ * @param { T } x
+ * @returns { x is Exclude }
+ */
+export function assertNotNullOrUndefined(x) {
+ expect(x).not.toEqual(null);
+ expect(x).not.toEqual(undefined);
+ return true;
+}
+
+/**
+ *
+ * @param { ReturnType["ez"] } ez
+ * @param { ReturnType["graph"] } graph
+ */
+export function createDefaultWorkflow(ez, graph) {
+ graph.clear();
+ const ckpt = ez.CheckpointLoaderSimple();
+
+ const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" });
+ const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" });
+
+ const empty = ez.EmptyLatentImage();
+ const sampler = ez.KSampler(
+ ckpt.outputs.MODEL,
+ pos.outputs.CONDITIONING,
+ neg.outputs.CONDITIONING,
+ empty.outputs.LATENT
+ );
+
+ const decode = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE);
+ const save = ez.SaveImage(decode.outputs.IMAGE);
+ graph.arrange();
+
+ return { ckpt, pos, neg, empty, sampler, decode, save };
+}
+
+export async function getNodeDefs() {
+ const { api } = require("../../web/scripts/api");
+ return api.getNodeDefs();
+}
+
+export async function getNodeDef(nodeId) {
+ return (await getNodeDefs())[nodeId];
+}
\ No newline at end of file
diff --git a/ComfyUI/tests-ui/utils/litegraph.js b/ComfyUI/tests-ui/utils/litegraph.js
new file mode 100644
index 0000000000000000000000000000000000000000..777f8c3ba136ab4564cb434e917c8241d94f5cf3
--- /dev/null
+++ b/ComfyUI/tests-ui/utils/litegraph.js
@@ -0,0 +1,36 @@
+const fs = require("fs");
+const path = require("path");
+const { nop } = require("../utils/nopProxy");
+
+function forEachKey(cb) {
+ for (const k of [
+ "LiteGraph",
+ "LGraph",
+ "LLink",
+ "LGraphNode",
+ "LGraphGroup",
+ "DragAndScale",
+ "LGraphCanvas",
+ "ContextMenu",
+ ]) {
+ cb(k);
+ }
+}
+
+export function setup(ctx) {
+ const lg = fs.readFileSync(path.resolve("../web/lib/litegraph.core.js"), "utf-8");
+ const globalTemp = {};
+ (function (console) {
+ eval(lg);
+ }).call(globalTemp, nop);
+
+ forEachKey((k) => (ctx[k] = globalTemp[k]));
+ require(path.resolve("../web/lib/litegraph.extensions.js"));
+}
+
+export function teardown(ctx) {
+ forEachKey((k) => delete ctx[k]);
+
+ // Clear document after each run
+ document.getElementsByTagName("html")[0].innerHTML = "";
+}
diff --git a/ComfyUI/tests-ui/utils/nopProxy.js b/ComfyUI/tests-ui/utils/nopProxy.js
new file mode 100644
index 0000000000000000000000000000000000000000..2502d9d03d6dbe09844e6940da5f4aede612fb6a
--- /dev/null
+++ b/ComfyUI/tests-ui/utils/nopProxy.js
@@ -0,0 +1,6 @@
+export const nop = new Proxy(function () {}, {
+ get: () => nop,
+ set: () => true,
+ apply: () => nop,
+ construct: () => nop,
+});
diff --git a/ComfyUI/tests-ui/utils/setup.js b/ComfyUI/tests-ui/utils/setup.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd150214a342cb21aa8ffb43a019db69e9e3b07f
--- /dev/null
+++ b/ComfyUI/tests-ui/utils/setup.js
@@ -0,0 +1,49 @@
+require("../../web/scripts/api");
+
+const fs = require("fs");
+const path = require("path");
+function* walkSync(dir) {
+ const files = fs.readdirSync(dir, { withFileTypes: true });
+ for (const file of files) {
+ if (file.isDirectory()) {
+ yield* walkSync(path.join(dir, file.name));
+ } else {
+ yield path.join(dir, file.name);
+ }
+ }
+}
+
+/**
+ * @typedef { import("../../web/types/comfy").ComfyObjectInfo } ComfyObjectInfo
+ */
+
+/**
+ * @param { { mockExtensions?: string[], mockNodeDefs?: Record } } config
+ */
+export function mockApi({ mockExtensions, mockNodeDefs } = {}) {
+ if (!mockExtensions) {
+ mockExtensions = Array.from(walkSync(path.resolve("../web/extensions/core")))
+ .filter((x) => x.endsWith(".js"))
+ .map((x) => path.relative(path.resolve("../web"), x));
+ }
+ if (!mockNodeDefs) {
+ mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./data/object_info.json")));
+ }
+
+ const events = new EventTarget();
+ const mockApi = {
+ addEventListener: events.addEventListener.bind(events),
+ removeEventListener: events.removeEventListener.bind(events),
+ dispatchEvent: events.dispatchEvent.bind(events),
+ getSystemStats: jest.fn(),
+ getExtensions: jest.fn(() => mockExtensions),
+ getNodeDefs: jest.fn(() => mockNodeDefs),
+ init: jest.fn(),
+ apiURL: jest.fn((x) => "../../web/" + x),
+ };
+ jest.mock("../../web/scripts/api", () => ({
+ get api() {
+ return mockApi;
+ },
+ }));
+}
diff --git a/ComfyUI/tests/README.md b/ComfyUI/tests/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2005fd45b2bbd249fb7f1dfff789b2ae236568ba
--- /dev/null
+++ b/ComfyUI/tests/README.md
@@ -0,0 +1,29 @@
+# Automated Testing
+
+## Running tests locally
+
+Additional requirements for running tests:
+```
+pip install pytest
+pip install websocket-client==1.6.1
+opencv-python==4.6.0.66
+scikit-image==0.21.0
+```
+Run inference tests:
+```
+pytest tests/inference
+```
+
+## Quality regression test
+Compares images in 2 directories to ensure they are the same
+
+1) Run an inference test to save a directory of "ground truth" images
+```
+ pytest tests/inference --output_dir tests/inference/baseline
+```
+2) Make code edits
+
+3) Run inference and quality comparison tests
+```
+pytest
+```
\ No newline at end of file
diff --git a/ComfyUI/tests/__init__.py b/ComfyUI/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/tests/compare/conftest.py b/ComfyUI/tests/compare/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd5078c9e6e432c7de2462c5641068bfa8e0aaee
--- /dev/null
+++ b/ComfyUI/tests/compare/conftest.py
@@ -0,0 +1,41 @@
+import os
+import pytest
+
+# Command line arguments for pytest
+def pytest_addoption(parser):
+ parser.addoption('--baseline_dir', action="store", default='tests/inference/baseline', help='Directory for ground-truth images')
+ parser.addoption('--test_dir', action="store", default='tests/inference/samples', help='Directory for images to test')
+ parser.addoption('--metrics_file', action="store", default='tests/metrics.md', help='Output file for metrics')
+ parser.addoption('--img_output_dir', action="store", default='tests/compare/samples', help='Output directory for diff metric images')
+
+# This initializes args at the beginning of the test session
+@pytest.fixture(scope="session", autouse=True)
+def args_pytest(pytestconfig):
+ args = {}
+ args['baseline_dir'] = pytestconfig.getoption('baseline_dir')
+ args['test_dir'] = pytestconfig.getoption('test_dir')
+ args['metrics_file'] = pytestconfig.getoption('metrics_file')
+ args['img_output_dir'] = pytestconfig.getoption('img_output_dir')
+
+ # Initialize metrics file
+ with open(args['metrics_file'], 'a') as f:
+ # if file is empty, write header
+ if os.stat(args['metrics_file']).st_size == 0:
+ f.write("| date | run | file | status | value | \n")
+ f.write("| --- | --- | --- | --- | --- | \n")
+
+ return args
+
+
+def gather_file_basenames(directory: str):
+ files = []
+ for file in os.listdir(directory):
+ if file.endswith(".png"):
+ files.append(file)
+ return files
+
+# Creates the list of baseline file names to use as a fixture
+def pytest_generate_tests(metafunc):
+ if "baseline_fname" in metafunc.fixturenames:
+ baseline_fnames = gather_file_basenames(metafunc.config.getoption("baseline_dir"))
+ metafunc.parametrize("baseline_fname", baseline_fnames)
diff --git a/ComfyUI/tests/compare/test_quality.py b/ComfyUI/tests/compare/test_quality.py
new file mode 100644
index 0000000000000000000000000000000000000000..92a2d5a8b021e98c6d24343033ac01020a5016bd
--- /dev/null
+++ b/ComfyUI/tests/compare/test_quality.py
@@ -0,0 +1,195 @@
+import datetime
+import numpy as np
+import os
+from PIL import Image
+import pytest
+from pytest import fixture
+from typing import Tuple, List
+
+from cv2 import imread, cvtColor, COLOR_BGR2RGB
+from skimage.metrics import structural_similarity as ssim
+
+
+"""
+This test suite compares images in 2 directories by file name
+The directories are specified by the command line arguments --baseline_dir and --test_dir
+
+"""
+# ssim: Structural Similarity Index
+# Returns a tuple of (ssim, diff_image)
+def ssim_score(img0: np.ndarray, img1: np.ndarray) -> Tuple[float, np.ndarray]:
+ score, diff = ssim(img0, img1, channel_axis=-1, full=True)
+ # rescale the difference image to 0-255 range
+ diff = (diff * 255).astype("uint8")
+ return score, diff
+
+# Metrics must return a tuple of (score, diff_image)
+METRICS = {"ssim": ssim_score}
+METRICS_PASS_THRESHOLD = {"ssim": 0.95}
+
+
+class TestCompareImageMetrics:
+ @fixture(scope="class")
+ def test_file_names(self, args_pytest):
+ test_dir = args_pytest['test_dir']
+ fnames = self.gather_file_basenames(test_dir)
+ yield fnames
+ del fnames
+
+ @fixture(scope="class", autouse=True)
+ def teardown(self, args_pytest):
+ yield
+ # Runs after all tests are complete
+ # Aggregate output files into a grid of images
+ baseline_dir = args_pytest['baseline_dir']
+ test_dir = args_pytest['test_dir']
+ img_output_dir = args_pytest['img_output_dir']
+ metrics_file = args_pytest['metrics_file']
+
+ grid_dir = os.path.join(img_output_dir, "grid")
+ os.makedirs(grid_dir, exist_ok=True)
+
+ for metric_dir in METRICS.keys():
+ metric_path = os.path.join(img_output_dir, metric_dir)
+ for file in os.listdir(metric_path):
+ if file.endswith(".png"):
+ score = self.lookup_score_from_fname(file, metrics_file)
+ image_file_list = []
+ image_file_list.append([
+ os.path.join(baseline_dir, file),
+ os.path.join(test_dir, file),
+ os.path.join(metric_path, file)
+ ])
+ # Create grid
+ image_list = [[Image.open(file) for file in files] for files in image_file_list]
+ grid = self.image_grid(image_list)
+ grid.save(os.path.join(grid_dir, f"{metric_dir}_{score:.3f}_{file}"))
+
+ # Tests run for each baseline file name
+ @fixture()
+ def fname(self, baseline_fname):
+ yield baseline_fname
+ del baseline_fname
+
+ def test_directories_not_empty(self, args_pytest):
+ baseline_dir = args_pytest['baseline_dir']
+ test_dir = args_pytest['test_dir']
+ assert len(os.listdir(baseline_dir)) != 0, f"Baseline directory {baseline_dir} is empty"
+ assert len(os.listdir(test_dir)) != 0, f"Test directory {test_dir} is empty"
+
+ def test_dir_has_all_matching_metadata(self, fname, test_file_names, args_pytest):
+ # Check that all files in baseline_dir have a file in test_dir with matching metadata
+ baseline_file_path = os.path.join(args_pytest['baseline_dir'], fname)
+ file_paths = [os.path.join(args_pytest['test_dir'], f) for f in test_file_names]
+ file_match = self.find_file_match(baseline_file_path, file_paths)
+ assert file_match is not None, f"Could not find a file in {args_pytest['test_dir']} with matching metadata to {baseline_file_path}"
+
+ # For a baseline image file, finds the corresponding file name in test_dir and
+ # compares the images using the metrics in METRICS
+ @pytest.mark.parametrize("metric", METRICS.keys())
+ def test_pipeline_compare(
+ self,
+ args_pytest,
+ fname,
+ test_file_names,
+ metric,
+ ):
+ baseline_dir = args_pytest['baseline_dir']
+ test_dir = args_pytest['test_dir']
+ metrics_output_file = args_pytest['metrics_file']
+ img_output_dir = args_pytest['img_output_dir']
+
+ baseline_file_path = os.path.join(baseline_dir, fname)
+
+ # Find file match
+ file_paths = [os.path.join(test_dir, f) for f in test_file_names]
+ test_file = self.find_file_match(baseline_file_path, file_paths)
+
+ # Run metrics
+ sample_baseline = self.read_img(baseline_file_path)
+ sample_secondary = self.read_img(test_file)
+
+ score, metric_img = METRICS[metric](sample_baseline, sample_secondary)
+ metric_status = score > METRICS_PASS_THRESHOLD[metric]
+
+ # Save metric values
+ with open(metrics_output_file, 'a') as f:
+ run_info = os.path.splitext(fname)[0]
+ metric_status_str = "PASS ✅" if metric_status else "FAIL ❌"
+ date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ f.write(f"| {date_str} | {run_info} | {metric} | {metric_status_str} | {score} | \n")
+
+ # Save metric image
+ metric_img_dir = os.path.join(img_output_dir, metric)
+ os.makedirs(metric_img_dir, exist_ok=True)
+ output_filename = f'{fname}'
+ Image.fromarray(metric_img).save(os.path.join(metric_img_dir, output_filename))
+
+ assert score > METRICS_PASS_THRESHOLD[metric]
+
+ def read_img(self, filename: str) -> np.ndarray:
+ cvImg = imread(filename)
+ cvImg = cvtColor(cvImg, COLOR_BGR2RGB)
+ return cvImg
+
+ def image_grid(self, img_list: list[list[Image.Image]]):
+ # imgs is a 2D list of images
+ # Assumes the input images are a rectangular grid of equal sized images
+ rows = len(img_list)
+ cols = len(img_list[0])
+
+ w, h = img_list[0][0].size
+ grid = Image.new('RGB', size=(cols*w, rows*h))
+
+ for i, row in enumerate(img_list):
+ for j, img in enumerate(row):
+ grid.paste(img, box=(j*w, i*h))
+ return grid
+
+ def lookup_score_from_fname(self,
+ fname: str,
+ metrics_output_file: str
+ ) -> float:
+ fname_basestr = os.path.splitext(fname)[0]
+ with open(metrics_output_file, 'r') as f:
+ for line in f:
+ if fname_basestr in line:
+ score = float(line.split('|')[5])
+ return score
+ raise ValueError(f"Could not find score for {fname} in {metrics_output_file}")
+
+ def gather_file_basenames(self, directory: str):
+ files = []
+ for file in os.listdir(directory):
+ if file.endswith(".png"):
+ files.append(file)
+ return files
+
+ def read_file_prompt(self, fname:str) -> str:
+ # Read prompt from image file metadata
+ img = Image.open(fname)
+ img.load()
+ return img.info['prompt']
+
+ def find_file_match(self, baseline_file: str, file_paths: List[str]):
+ # Find a file in file_paths with matching metadata to baseline_file
+ baseline_prompt = self.read_file_prompt(baseline_file)
+
+ # Do not match empty prompts
+ if baseline_prompt is None or baseline_prompt == "":
+ return None
+
+ # Find file match
+ # Reorder test_file_names so that the file with matching name is first
+ # This is an optimization because matching file names are more likely
+ # to have matching metadata if they were generated with the same script
+ basename = os.path.basename(baseline_file)
+ file_path_basenames = [os.path.basename(f) for f in file_paths]
+ if basename in file_path_basenames:
+ match_index = file_path_basenames.index(basename)
+ file_paths.insert(0, file_paths.pop(match_index))
+
+ for f in file_paths:
+ test_file_prompt = self.read_file_prompt(f)
+ if baseline_prompt == test_file_prompt:
+ return f
\ No newline at end of file
diff --git a/ComfyUI/tests/conftest.py b/ComfyUI/tests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a35880af5bf86f4a1680b6835b1dcf2e4ef59c8
--- /dev/null
+++ b/ComfyUI/tests/conftest.py
@@ -0,0 +1,36 @@
+import os
+import pytest
+
+# Command line arguments for pytest
+def pytest_addoption(parser):
+ parser.addoption('--output_dir', action="store", default='tests/inference/samples', help='Output directory for generated images')
+ parser.addoption("--listen", type=str, default="127.0.0.1", metavar="IP", nargs="?", const="0.0.0.0", help="Specify the IP address to listen on (default: 127.0.0.1). If --listen is provided without an argument, it defaults to 0.0.0.0. (listens on all)")
+ parser.addoption("--port", type=int, default=8188, help="Set the listen port.")
+
+# This initializes args at the beginning of the test session
+@pytest.fixture(scope="session", autouse=True)
+def args_pytest(pytestconfig):
+ args = {}
+ args['output_dir'] = pytestconfig.getoption('output_dir')
+ args['listen'] = pytestconfig.getoption('listen')
+ args['port'] = pytestconfig.getoption('port')
+
+ os.makedirs(args['output_dir'], exist_ok=True)
+
+ return args
+
+def pytest_collection_modifyitems(items):
+ # Modifies items so tests run in the correct order
+
+ LAST_TESTS = ['test_quality']
+
+ # Move the last items to the end
+ last_items = []
+ for test_name in LAST_TESTS:
+ for item in items.copy():
+ print(item.module.__name__, item)
+ if item.module.__name__ == test_name:
+ last_items.append(item)
+ items.remove(item)
+
+ items.extend(last_items)
diff --git a/ComfyUI/tests/inference/__init__.py b/ComfyUI/tests/inference/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ComfyUI/tests/inference/graphs/default_graph_sdxl1_0.json b/ComfyUI/tests/inference/graphs/default_graph_sdxl1_0.json
new file mode 100644
index 0000000000000000000000000000000000000000..c06c6829c6253cc71982f3714620bd10dd41bd73
--- /dev/null
+++ b/ComfyUI/tests/inference/graphs/default_graph_sdxl1_0.json
@@ -0,0 +1,144 @@
+{
+ "4": {
+ "inputs": {
+ "ckpt_name": "sd_xl_base_1.0.safetensors"
+ },
+ "class_type": "CheckpointLoaderSimple"
+ },
+ "5": {
+ "inputs": {
+ "width": 1024,
+ "height": 1024,
+ "batch_size": 1
+ },
+ "class_type": "EmptyLatentImage"
+ },
+ "6": {
+ "inputs": {
+ "text": "a photo of a cat",
+ "clip": [
+ "4",
+ 1
+ ]
+ },
+ "class_type": "CLIPTextEncode"
+ },
+ "10": {
+ "inputs": {
+ "add_noise": "enable",
+ "noise_seed": 42,
+ "steps": 20,
+ "cfg": 7.5,
+ "sampler_name": "euler",
+ "scheduler": "normal",
+ "start_at_step": 0,
+ "end_at_step": 32,
+ "return_with_leftover_noise": "enable",
+ "model": [
+ "4",
+ 0
+ ],
+ "positive": [
+ "6",
+ 0
+ ],
+ "negative": [
+ "15",
+ 0
+ ],
+ "latent_image": [
+ "5",
+ 0
+ ]
+ },
+ "class_type": "KSamplerAdvanced"
+ },
+ "12": {
+ "inputs": {
+ "samples": [
+ "14",
+ 0
+ ],
+ "vae": [
+ "4",
+ 2
+ ]
+ },
+ "class_type": "VAEDecode"
+ },
+ "13": {
+ "inputs": {
+ "filename_prefix": "test_inference",
+ "images": [
+ "12",
+ 0
+ ]
+ },
+ "class_type": "SaveImage"
+ },
+ "14": {
+ "inputs": {
+ "add_noise": "disable",
+ "noise_seed": 42,
+ "steps": 20,
+ "cfg": 7.5,
+ "sampler_name": "euler",
+ "scheduler": "normal",
+ "start_at_step": 32,
+ "end_at_step": 10000,
+ "return_with_leftover_noise": "disable",
+ "model": [
+ "16",
+ 0
+ ],
+ "positive": [
+ "17",
+ 0
+ ],
+ "negative": [
+ "20",
+ 0
+ ],
+ "latent_image": [
+ "10",
+ 0
+ ]
+ },
+ "class_type": "KSamplerAdvanced"
+ },
+ "15": {
+ "inputs": {
+ "conditioning": [
+ "6",
+ 0
+ ]
+ },
+ "class_type": "ConditioningZeroOut"
+ },
+ "16": {
+ "inputs": {
+ "ckpt_name": "sd_xl_refiner_1.0.safetensors"
+ },
+ "class_type": "CheckpointLoaderSimple"
+ },
+ "17": {
+ "inputs": {
+ "text": "a photo of a cat",
+ "clip": [
+ "16",
+ 1
+ ]
+ },
+ "class_type": "CLIPTextEncode"
+ },
+ "20": {
+ "inputs": {
+ "text": "",
+ "clip": [
+ "16",
+ 1
+ ]
+ },
+ "class_type": "CLIPTextEncode"
+ }
+ }
\ No newline at end of file
diff --git a/ComfyUI/tests/inference/test_inference.py b/ComfyUI/tests/inference/test_inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..141cc5c7eac02e5fe98394f7fc8d76cf820138bb
--- /dev/null
+++ b/ComfyUI/tests/inference/test_inference.py
@@ -0,0 +1,239 @@
+from copy import deepcopy
+from io import BytesIO
+from urllib import request
+import numpy
+import os
+from PIL import Image
+import pytest
+from pytest import fixture
+import time
+import torch
+from typing import Union
+import json
+import subprocess
+import websocket #NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
+import uuid
+import urllib.request
+import urllib.parse
+
+
+from comfy.samplers import KSampler
+
+"""
+These tests generate and save images through a range of parameters
+"""
+
+class ComfyGraph:
+ def __init__(self,
+ graph: dict,
+ sampler_nodes: list[str],
+ ):
+ self.graph = graph
+ self.sampler_nodes = sampler_nodes
+
+ def set_prompt(self, prompt, negative_prompt=None):
+ # Sets the prompt for the sampler nodes (eg. base and refiner)
+ for node in self.sampler_nodes:
+ prompt_node = self.graph[node]['inputs']['positive'][0]
+ self.graph[prompt_node]['inputs']['text'] = prompt
+ if negative_prompt:
+ negative_prompt_node = self.graph[node]['inputs']['negative'][0]
+ self.graph[negative_prompt_node]['inputs']['text'] = negative_prompt
+
+ def set_sampler_name(self, sampler_name:str, ):
+ # sets the sampler name for the sampler nodes (eg. base and refiner)
+ for node in self.sampler_nodes:
+ self.graph[node]['inputs']['sampler_name'] = sampler_name
+
+ def set_scheduler(self, scheduler:str):
+ # sets the sampler name for the sampler nodes (eg. base and refiner)
+ for node in self.sampler_nodes:
+ self.graph[node]['inputs']['scheduler'] = scheduler
+
+ def set_filename_prefix(self, prefix:str):
+ # sets the filename prefix for the save nodes
+ for node in self.graph:
+ if self.graph[node]['class_type'] == 'SaveImage':
+ self.graph[node]['inputs']['filename_prefix'] = prefix
+
+
+class ComfyClient:
+ # From examples/websockets_api_example.py
+
+ def connect(self,
+ listen:str = '127.0.0.1',
+ port:Union[str,int] = 8188,
+ client_id: str = str(uuid.uuid4())
+ ):
+ self.client_id = client_id
+ self.server_address = f"{listen}:{port}"
+ ws = websocket.WebSocket()
+ ws.connect("ws://{}/ws?clientId={}".format(self.server_address, self.client_id))
+ self.ws = ws
+
+ def queue_prompt(self, prompt):
+ p = {"prompt": prompt, "client_id": self.client_id}
+ data = json.dumps(p).encode('utf-8')
+ req = urllib.request.Request("http://{}/prompt".format(self.server_address), data=data)
+ return json.loads(urllib.request.urlopen(req).read())
+
+ def get_image(self, filename, subfolder, folder_type):
+ data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
+ url_values = urllib.parse.urlencode(data)
+ with urllib.request.urlopen("http://{}/view?{}".format(self.server_address, url_values)) as response:
+ return response.read()
+
+ def get_history(self, prompt_id):
+ with urllib.request.urlopen("http://{}/history/{}".format(self.server_address, prompt_id)) as response:
+ return json.loads(response.read())
+
+ def get_images(self, graph, save=True):
+ prompt = graph
+ if not save:
+ # Replace save nodes with preview nodes
+ prompt_str = json.dumps(prompt)
+ prompt_str = prompt_str.replace('SaveImage', 'PreviewImage')
+ prompt = json.loads(prompt_str)
+
+ prompt_id = self.queue_prompt(prompt)['prompt_id']
+ output_images = {}
+ while True:
+ out = self.ws.recv()
+ if isinstance(out, str):
+ message = json.loads(out)
+ if message['type'] == 'executing':
+ data = message['data']
+ if data['node'] is None and data['prompt_id'] == prompt_id:
+ break #Execution is done
+ else:
+ continue #previews are binary data
+
+ history = self.get_history(prompt_id)[prompt_id]
+ for o in history['outputs']:
+ for node_id in history['outputs']:
+ node_output = history['outputs'][node_id]
+ if 'images' in node_output:
+ images_output = []
+ for image in node_output['images']:
+ image_data = self.get_image(image['filename'], image['subfolder'], image['type'])
+ images_output.append(image_data)
+ output_images[node_id] = images_output
+
+ return output_images
+
+#
+# Initialize graphs
+#
+default_graph_file = 'tests/inference/graphs/default_graph_sdxl1_0.json'
+with open(default_graph_file, 'r') as file:
+ default_graph = json.loads(file.read())
+DEFAULT_COMFY_GRAPH = ComfyGraph(graph=default_graph, sampler_nodes=['10','14'])
+DEFAULT_COMFY_GRAPH_ID = os.path.splitext(os.path.basename(default_graph_file))[0]
+
+#
+# Loop through these variables
+#
+comfy_graph_list = [DEFAULT_COMFY_GRAPH]
+comfy_graph_ids = [DEFAULT_COMFY_GRAPH_ID]
+prompt_list = [
+ 'a painting of a cat',
+]
+
+sampler_list = KSampler.SAMPLERS
+scheduler_list = KSampler.SCHEDULERS
+
+@pytest.mark.inference
+@pytest.mark.parametrize("sampler", sampler_list)
+@pytest.mark.parametrize("scheduler", scheduler_list)
+@pytest.mark.parametrize("prompt", prompt_list)
+class TestInference:
+ #
+ # Initialize server and client
+ #
+ @fixture(scope="class", autouse=True)
+ def _server(self, args_pytest):
+ # Start server
+ p = subprocess.Popen([
+ 'python','main.py',
+ '--output-directory', args_pytest["output_dir"],
+ '--listen', args_pytest["listen"],
+ '--port', str(args_pytest["port"]),
+ ])
+ yield
+ p.kill()
+ torch.cuda.empty_cache()
+
+ def start_client(self, listen:str, port:int):
+ # Start client
+ comfy_client = ComfyClient()
+ # Connect to server (with retries)
+ n_tries = 5
+ for i in range(n_tries):
+ time.sleep(4)
+ try:
+ comfy_client.connect(listen=listen, port=port)
+ except ConnectionRefusedError as e:
+ print(e)
+ print(f"({i+1}/{n_tries}) Retrying...")
+ else:
+ break
+ return comfy_client
+
+ #
+ # Client and graph fixtures with server warmup
+ #
+ # Returns a "_client_graph", which is client-graph pair corresponding to an initialized server
+ # The "graph" is the default graph
+ @fixture(scope="class", params=comfy_graph_list, ids=comfy_graph_ids, autouse=True)
+ def _client_graph(self, request, args_pytest, _server) -> (ComfyClient, ComfyGraph):
+ comfy_graph = request.param
+
+ # Start client
+ comfy_client = self.start_client(args_pytest["listen"], args_pytest["port"])
+
+ # Warm up pipeline
+ comfy_client.get_images(graph=comfy_graph.graph, save=False)
+
+ yield comfy_client, comfy_graph
+ del comfy_client
+ del comfy_graph
+ torch.cuda.empty_cache()
+
+ @fixture
+ def client(self, _client_graph):
+ client = _client_graph[0]
+ yield client
+
+ @fixture
+ def comfy_graph(self, _client_graph):
+ # avoid mutating the graph
+ graph = deepcopy(_client_graph[1])
+ yield graph
+
+ def test_comfy(
+ self,
+ client,
+ comfy_graph,
+ sampler,
+ scheduler,
+ prompt,
+ request
+ ):
+ test_info = request.node.name
+ comfy_graph.set_filename_prefix(test_info)
+ # Settings for comfy graph
+ comfy_graph.set_sampler_name(sampler)
+ comfy_graph.set_scheduler(scheduler)
+ comfy_graph.set_prompt(prompt)
+
+ # Generate
+ images = client.get_images(comfy_graph.graph)
+
+ assert len(images) != 0, "No images generated"
+ # assert all images are not blank
+ for images_output in images.values():
+ for image_data in images_output:
+ pil_image = Image.open(BytesIO(image_data))
+ assert numpy.array(pil_image).any() != 0, "Image is blank"
+
+
diff --git a/ComfyUI/web/extensions/core/clipspace.js b/ComfyUI/web/extensions/core/clipspace.js
new file mode 100644
index 0000000000000000000000000000000000000000..e376a02f70db855e056cb3ccc0c57b6627a190f5
--- /dev/null
+++ b/ComfyUI/web/extensions/core/clipspace.js
@@ -0,0 +1,166 @@
+import { app } from "../../scripts/app.js";
+import { ComfyDialog, $el } from "../../scripts/ui.js";
+import { ComfyApp } from "../../scripts/app.js";
+
+export class ClipspaceDialog extends ComfyDialog {
+ static items = [];
+ static instance = null;
+
+ static registerButton(name, contextPredicate, callback) {
+ const item =
+ $el("button", {
+ type: "button",
+ textContent: name,
+ contextPredicate: contextPredicate,
+ onclick: callback
+ })
+
+ ClipspaceDialog.items.push(item);
+ }
+
+ static invalidatePreview() {
+ if(ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0) {
+ const img_preview = document.getElementById("clipspace_preview");
+ if(img_preview) {
+ img_preview.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
+ img_preview.style.maxHeight = "100%";
+ img_preview.style.maxWidth = "100%";
+ }
+ }
+ }
+
+ static invalidate() {
+ if(ClipspaceDialog.instance) {
+ const self = ClipspaceDialog.instance;
+ // allow reconstruct controls when copying from non-image to image content.
+ const children = $el("div.comfy-modal-content", [ self.createImgSettings(), ...self.createButtons() ]);
+
+ if(self.element) {
+ // update
+ self.element.removeChild(self.element.firstChild);
+ self.element.appendChild(children);
+ }
+ else {
+ // new
+ self.element = $el("div.comfy-modal", { parent: document.body }, [children,]);
+ }
+
+ if(self.element.children[0].children.length <= 1) {
+ self.element.children[0].appendChild($el("p", {}, ["Unable to find the features to edit content of a format stored in the current Clipspace."]));
+ }
+
+ ClipspaceDialog.invalidatePreview();
+ }
+ }
+
+ constructor() {
+ super();
+ }
+
+ createButtons(self) {
+ const buttons = [];
+
+ for(let idx in ClipspaceDialog.items) {
+ const item = ClipspaceDialog.items[idx];
+ if(!item.contextPredicate || item.contextPredicate())
+ buttons.push(ClipspaceDialog.items[idx]);
+ }
+
+ buttons.push(
+ $el("button", {
+ type: "button",
+ textContent: "Close",
+ onclick: () => { this.close(); }
+ })
+ );
+
+ return buttons;
+ }
+
+ createImgSettings() {
+ if(ComfyApp.clipspace.imgs) {
+ const combo_items = [];
+ const imgs = ComfyApp.clipspace.imgs;
+
+ for(let i=0; i < imgs.length; i++) {
+ combo_items.push($el("option", {value:i}, [`${i}`]));
+ }
+
+ const combo1 = $el("select",
+ {id:"clipspace_img_selector", onchange:(event) => {
+ ComfyApp.clipspace['selectedIndex'] = event.target.selectedIndex;
+ ClipspaceDialog.invalidatePreview();
+ } }, combo_items);
+
+ const row1 =
+ $el("tr", {},
+ [
+ $el("td", {}, [$el("font", {color:"white"}, ["Select Image"])]),
+ $el("td", {}, [combo1])
+ ]);
+
+
+ const combo2 = $el("select",
+ {id:"clipspace_img_paste_mode", onchange:(event) => {
+ ComfyApp.clipspace['img_paste_mode'] = event.target.value;
+ } },
+ [
+ $el("option", {value:'selected'}, 'selected'),
+ $el("option", {value:'all'}, 'all')
+ ]);
+ combo2.value = ComfyApp.clipspace['img_paste_mode'];
+
+ const row2 =
+ $el("tr", {},
+ [
+ $el("td", {}, [$el("font", {color:"white"}, ["Paste Mode"])]),
+ $el("td", {}, [combo2])
+ ]);
+
+ const td = $el("td", {align:'center', width:'100px', height:'100px', colSpan:'2'},
+ [ $el("img",{id:"clipspace_preview", ondragstart:() => false},[]) ]);
+
+ const row3 =
+ $el("tr", {}, [td]);
+
+ return $el("table", {}, [row1, row2, row3]);
+ }
+ else {
+ return [];
+ }
+ }
+
+ createImgPreview() {
+ if(ComfyApp.clipspace.imgs) {
+ return $el("img",{id:"clipspace_preview", ondragstart:() => false});
+ }
+ else
+ return [];
+ }
+
+ show() {
+ const img_preview = document.getElementById("clipspace_preview");
+ ClipspaceDialog.invalidate();
+
+ this.element.style.display = "block";
+ }
+}
+
+app.registerExtension({
+ name: "Comfy.Clipspace",
+ init(app) {
+ app.openClipspace =
+ function () {
+ if(!ClipspaceDialog.instance) {
+ ClipspaceDialog.instance = new ClipspaceDialog(app);
+ ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate;
+ }
+
+ if(ComfyApp.clipspace) {
+ ClipspaceDialog.instance.show();
+ }
+ else
+ app.ui.dialog.show("Clipspace is Empty!");
+ };
+ }
+});
\ No newline at end of file
diff --git a/ComfyUI/web/extensions/core/colorPalette.js b/ComfyUI/web/extensions/core/colorPalette.js
new file mode 100644
index 0000000000000000000000000000000000000000..b8d83613d4b06116bdea66c20c177ed2e14d0677
--- /dev/null
+++ b/ComfyUI/web/extensions/core/colorPalette.js
@@ -0,0 +1,757 @@
+import {app} from "../../scripts/app.js";
+import {$el} from "../../scripts/ui.js";
+
+// Manage color palettes
+
+const colorPalettes = {
+ "dark": {
+ "id": "dark",
+ "name": "Dark (Default)",
+ "colors": {
+ "node_slot": {
+ "CLIP": "#FFD500", // bright yellow
+ "CLIP_VISION": "#A8DADC", // light blue-gray
+ "CLIP_VISION_OUTPUT": "#ad7452", // rusty brown-orange
+ "CONDITIONING": "#FFA931", // vibrant orange-yellow
+ "CONTROL_NET": "#6EE7B7", // soft mint green
+ "IMAGE": "#64B5F6", // bright sky blue
+ "LATENT": "#FF9CF9", // light pink-purple
+ "MASK": "#81C784", // muted green
+ "MODEL": "#B39DDB", // light lavender-purple
+ "STYLE_MODEL": "#C2FFAE", // light green-yellow
+ "VAE": "#FF6E6E", // bright red
+ "TAESD": "#DCC274", // cheesecake
+ },
+ "litegraph_base": {
+ "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=",
+ "CLEAR_BACKGROUND_COLOR": "#222",
+ "NODE_TITLE_COLOR": "#999",
+ "NODE_SELECTED_TITLE_COLOR": "#FFF",
+ "NODE_TEXT_SIZE": 14,
+ "NODE_TEXT_COLOR": "#AAA",
+ "NODE_SUBTEXT_SIZE": 12,
+ "NODE_DEFAULT_COLOR": "#333",
+ "NODE_DEFAULT_BGCOLOR": "#353535",
+ "NODE_DEFAULT_BOXCOLOR": "#666",
+ "NODE_DEFAULT_SHAPE": "box",
+ "NODE_BOX_OUTLINE_COLOR": "#FFF",
+ "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
+ "DEFAULT_GROUP_FONT": 24,
+
+ "WIDGET_BGCOLOR": "#222",
+ "WIDGET_OUTLINE_COLOR": "#666",
+ "WIDGET_TEXT_COLOR": "#DDD",
+ "WIDGET_SECONDARY_TEXT_COLOR": "#999",
+
+ "LINK_COLOR": "#9A9",
+ "EVENT_LINK_COLOR": "#A86",
+ "CONNECTING_LINK_COLOR": "#AFA",
+ },
+ "comfy_base": {
+ "fg-color": "#fff",
+ "bg-color": "#202020",
+ "comfy-menu-bg": "#353535",
+ "comfy-input-bg": "#222",
+ "input-text": "#ddd",
+ "descrip-text": "#999",
+ "drag-text": "#ccc",
+ "error-text": "#ff4444",
+ "border-color": "#4e4e4e",
+ "tr-even-bg-color": "#222",
+ "tr-odd-bg-color": "#353535",
+ }
+ },
+ },
+ "light": {
+ "id": "light",
+ "name": "Light",
+ "colors": {
+ "node_slot": {
+ "CLIP": "#FFA726", // orange
+ "CLIP_VISION": "#5C6BC0", // indigo
+ "CLIP_VISION_OUTPUT": "#8D6E63", // brown
+ "CONDITIONING": "#EF5350", // red
+ "CONTROL_NET": "#66BB6A", // green
+ "IMAGE": "#42A5F5", // blue
+ "LATENT": "#AB47BC", // purple
+ "MASK": "#9CCC65", // light green
+ "MODEL": "#7E57C2", // deep purple
+ "STYLE_MODEL": "#D4E157", // lime
+ "VAE": "#FF7043", // deep orange
+ },
+ "litegraph_base": {
+ "BACKGROUND_IMAGE": "data:image/gif;base64,R0lGODlhZABkALMAAAAAAP///+vr6+rq6ujo6Ofn5+bm5uXl5d3d3f///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAABkAGQAAAT/UMhJq7046827HkcoHkYxjgZhnGG6si5LqnIM0/fL4qwwIMAg0CAsEovBIxKhRDaNy2GUOX0KfVFrssrNdpdaqTeKBX+dZ+jYvEaTf+y4W66mC8PUdrE879f9d2mBeoNLfH+IhYBbhIx2jkiHiomQlGKPl4uZe3CaeZifnnijgkESBqipqqusra6vsLGys62SlZO4t7qbuby7CLa+wqGWxL3Gv3jByMOkjc2lw8vOoNSi0czAncXW3Njdx9Pf48/Z4Kbbx+fQ5evZ4u3k1fKR6cn03vHlp7T9/v8A/8Gbp4+gwXoFryXMB2qgwoMMHyKEqA5fxX322FG8tzBcRnMW/zlulPbRncmQGidKjMjyYsOSKEF2FBlJQMCbOHP6c9iSZs+UnGYCdbnSo1CZI5F64kn0p1KnTH02nSoV3dGTV7FFHVqVq1dtWcMmVQZTbNGu72zqXMuW7danVL+6e4t1bEy6MeueBYLXrNO5Ze36jQtWsOG97wIj1vt3St/DjTEORss4nNq2mDP3e7w4r1bFkSET5hy6s2TRlD2/mSxXtSHQhCunXo26NevCpmvD/UU6tuullzULH76q92zdZG/Ltv1a+W+osI/nRmyc+fRi1Xdbh+68+0vv10dH3+77KD/i6IdnX669/frn5Zsjh4/2PXju8+8bzc9/6fj27LFnX11/+IUnXWl7BJfegm79FyB9JOl3oHgSklefgxAC+FmFGpqHIYcCfkhgfCohSKKJVo044YUMttggiBkmp6KFXw1oII24oYhjiDByaKOOHcp3Y5BD/njikSkO+eBREQAAOw==",
+ "CLEAR_BACKGROUND_COLOR": "lightgray",
+ "NODE_TITLE_COLOR": "#222",
+ "NODE_SELECTED_TITLE_COLOR": "#000",
+ "NODE_TEXT_SIZE": 14,
+ "NODE_TEXT_COLOR": "#444",
+ "NODE_SUBTEXT_SIZE": 12,
+ "NODE_DEFAULT_COLOR": "#F7F7F7",
+ "NODE_DEFAULT_BGCOLOR": "#F5F5F5",
+ "NODE_DEFAULT_BOXCOLOR": "#CCC",
+ "NODE_DEFAULT_SHAPE": "box",
+ "NODE_BOX_OUTLINE_COLOR": "#000",
+ "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.1)",
+ "DEFAULT_GROUP_FONT": 24,
+
+ "WIDGET_BGCOLOR": "#D4D4D4",
+ "WIDGET_OUTLINE_COLOR": "#999",
+ "WIDGET_TEXT_COLOR": "#222",
+ "WIDGET_SECONDARY_TEXT_COLOR": "#555",
+
+ "LINK_COLOR": "#4CAF50",
+ "EVENT_LINK_COLOR": "#FF9800",
+ "CONNECTING_LINK_COLOR": "#2196F3",
+ },
+ "comfy_base": {
+ "fg-color": "#222",
+ "bg-color": "#DDD",
+ "comfy-menu-bg": "#F5F5F5",
+ "comfy-input-bg": "#C9C9C9",
+ "input-text": "#222",
+ "descrip-text": "#444",
+ "drag-text": "#555",
+ "error-text": "#F44336",
+ "border-color": "#888",
+ "tr-even-bg-color": "#f9f9f9",
+ "tr-odd-bg-color": "#fff",
+ }
+ },
+ },
+ "solarized": {
+ "id": "solarized",
+ "name": "Solarized",
+ "colors": {
+ "node_slot": {
+ "CLIP": "#2AB7CA", // light blue
+ "CLIP_VISION": "#6c71c4", // blue violet
+ "CLIP_VISION_OUTPUT": "#859900", // olive green
+ "CONDITIONING": "#d33682", // magenta
+ "CONTROL_NET": "#d1ffd7", // light mint green
+ "IMAGE": "#5940bb", // deep blue violet
+ "LATENT": "#268bd2", // blue
+ "MASK": "#CCC9E7", // light purple-gray
+ "MODEL": "#dc322f", // red
+ "STYLE_MODEL": "#1a998a", // teal
+ "UPSCALE_MODEL": "#054A29", // dark green
+ "VAE": "#facfad", // light pink-orange
+ },
+ "litegraph_base": {
+ "NODE_TITLE_COLOR": "#fdf6e3", // Base3
+ "NODE_SELECTED_TITLE_COLOR": "#A9D400",
+ "NODE_TEXT_SIZE": 14,
+ "NODE_TEXT_COLOR": "#657b83", // Base00
+ "NODE_SUBTEXT_SIZE": 12,
+ "NODE_DEFAULT_COLOR": "#094656",
+ "NODE_DEFAULT_BGCOLOR": "#073642", // Base02
+ "NODE_DEFAULT_BOXCOLOR": "#839496", // Base0
+ "NODE_DEFAULT_SHAPE": "box",
+ "NODE_BOX_OUTLINE_COLOR": "#fdf6e3", // Base3
+ "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
+ "DEFAULT_GROUP_FONT": 24,
+
+ "WIDGET_BGCOLOR": "#002b36", // Base03
+ "WIDGET_OUTLINE_COLOR": "#839496", // Base0
+ "WIDGET_TEXT_COLOR": "#fdf6e3", // Base3
+ "WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1", // Base1
+
+ "LINK_COLOR": "#2aa198", // Solarized Cyan
+ "EVENT_LINK_COLOR": "#268bd2", // Solarized Blue
+ "CONNECTING_LINK_COLOR": "#859900", // Solarized Green
+ },
+ "comfy_base": {
+ "fg-color": "#fdf6e3", // Base3
+ "bg-color": "#002b36", // Base03
+ "comfy-menu-bg": "#073642", // Base02
+ "comfy-input-bg": "#002b36", // Base03
+ "input-text": "#93a1a1", // Base1
+ "descrip-text": "#586e75", // Base01
+ "drag-text": "#839496", // Base0
+ "error-text": "#dc322f", // Solarized Red
+ "border-color": "#657b83", // Base00
+ "tr-even-bg-color": "#002b36",
+ "tr-odd-bg-color": "#073642",
+ }
+ },
+ },
+ "arc": {
+ "id": "arc",
+ "name": "Arc",
+ "colors": {
+ "node_slot": {
+ "BOOLEAN": "",
+ "CLIP": "#eacb8b",
+ "CLIP_VISION": "#A8DADC",
+ "CLIP_VISION_OUTPUT": "#ad7452",
+ "CONDITIONING": "#cf876f",
+ "CONTROL_NET": "#00d78d",
+ "CONTROL_NET_WEIGHTS": "",
+ "FLOAT": "",
+ "GLIGEN": "",
+ "IMAGE": "#80a1c0",
+ "IMAGEUPLOAD": "",
+ "INT": "",
+ "LATENT": "#b38ead",
+ "LATENT_KEYFRAME": "",
+ "MASK": "#a3bd8d",
+ "MODEL": "#8978a7",
+ "SAMPLER": "",
+ "SIGMAS": "",
+ "STRING": "",
+ "STYLE_MODEL": "#C2FFAE",
+ "T2I_ADAPTER_WEIGHTS": "",
+ "TAESD": "#DCC274",
+ "TIMESTEP_KEYFRAME": "",
+ "UPSCALE_MODEL": "",
+ "VAE": "#be616b"
+ },
+ "litegraph_base": {
+ "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABcklEQVR4nO3YMUoDARgF4RfxBqZI6/0vZqFn0MYtrLIQMFN8U6V4LAtD+Jm9XG/v30OGl2e/AP7yevz4+vx45nvgF/+QGITEICQGITEIiUFIjNNC3q43u3/YnRJyPOzeQ+0e220nhRzReC8e7R7bbdvl+Jal1Bs46jEIiUFIDEJiEBKDkBhKPbZT6qHdptRTu02p53DUYxASg5AYhMQgJAYhMZR6bKfUQ7tNqad2m1LP4ajHICQGITEIiUFIDEJiKPXYTqmHdptST+02pZ7DUY9BSAxCYhASg5AYhMRQ6rGdUg/tNqWe2m1KPYejHoOQGITEICQGITEIiaHUYzulHtptSj2125R6Dkc9BiExCIlBSAxCYhASQ6nHdko9tNuUemq3KfUcjnoMQmIQEoOQGITEICSGUo/tlHpotyn11G5T6jkc9RiExCAkBiExCIlBSAylHtsp9dBuU+qp3abUczjqMQiJQUgMQmIQEoOQGITE+AHFISNQrFTGuwAAAABJRU5ErkJggg==",
+ "CLEAR_BACKGROUND_COLOR": "#2b2f38",
+ "NODE_TITLE_COLOR": "#b2b7bd",
+ "NODE_SELECTED_TITLE_COLOR": "#FFF",
+ "NODE_TEXT_SIZE": 14,
+ "NODE_TEXT_COLOR": "#AAA",
+ "NODE_SUBTEXT_SIZE": 12,
+ "NODE_DEFAULT_COLOR": "#2b2f38",
+ "NODE_DEFAULT_BGCOLOR": "#242730",
+ "NODE_DEFAULT_BOXCOLOR": "#6e7581",
+ "NODE_DEFAULT_SHAPE": "box",
+ "NODE_BOX_OUTLINE_COLOR": "#FFF",
+ "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
+ "DEFAULT_GROUP_FONT": 22,
+ "WIDGET_BGCOLOR": "#2b2f38",
+ "WIDGET_OUTLINE_COLOR": "#6e7581",
+ "WIDGET_TEXT_COLOR": "#DDD",
+ "WIDGET_SECONDARY_TEXT_COLOR": "#b2b7bd",
+ "LINK_COLOR": "#9A9",
+ "EVENT_LINK_COLOR": "#A86",
+ "CONNECTING_LINK_COLOR": "#AFA"
+ },
+ "comfy_base": {
+ "fg-color": "#fff",
+ "bg-color": "#2b2f38",
+ "comfy-menu-bg": "#242730",
+ "comfy-input-bg": "#2b2f38",
+ "input-text": "#ddd",
+ "descrip-text": "#b2b7bd",
+ "drag-text": "#ccc",
+ "error-text": "#ff4444",
+ "border-color": "#6e7581",
+ "tr-even-bg-color": "#2b2f38",
+ "tr-odd-bg-color": "#242730"
+ }
+ },
+ },
+ "nord": {
+ "id": "nord",
+ "name": "Nord",
+ "colors": {
+ "node_slot": {
+ "BOOLEAN": "",
+ "CLIP": "#eacb8b",
+ "CLIP_VISION": "#A8DADC",
+ "CLIP_VISION_OUTPUT": "#ad7452",
+ "CONDITIONING": "#cf876f",
+ "CONTROL_NET": "#00d78d",
+ "CONTROL_NET_WEIGHTS": "",
+ "FLOAT": "",
+ "GLIGEN": "",
+ "IMAGE": "#80a1c0",
+ "IMAGEUPLOAD": "",
+ "INT": "",
+ "LATENT": "#b38ead",
+ "LATENT_KEYFRAME": "",
+ "MASK": "#a3bd8d",
+ "MODEL": "#8978a7",
+ "SAMPLER": "",
+ "SIGMAS": "",
+ "STRING": "",
+ "STYLE_MODEL": "#C2FFAE",
+ "T2I_ADAPTER_WEIGHTS": "",
+ "TAESD": "#DCC274",
+ "TIMESTEP_KEYFRAME": "",
+ "UPSCALE_MODEL": "",
+ "VAE": "#be616b"
+ },
+ "litegraph_base": {
+ "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFu2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUwNDFhMmZjLTEzNzQtMTk0ZC1hZWY4LTYxMzM1MTVmNjUwMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MDQxYTJmYy0xMzc0LTE5NGQtYWVmOC02MTMzNTE1ZjY1MDAiIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz73jWg/AAAAyUlEQVR42u3WKwoAIBRFQRdiMb1idv9Lsxn9gEFw4Dbb8JCTojbbXEJwjJVL2HKwYMGCBQuWLbDmjr+9zrBGjHl1WVcvy2DBggULFizTWQpewSt4HzwsgwULFiwFr7MUvMtS8D54WLBgGSxYCl7BK3iXZbBgwYIFC5bpLAWv4BW8Dx6WwYIFC5aC11kK3mUpeB88LFiwDBYsBa/gFbzLMliwYMGCBct0loJX8AreBw/LYMGCBUvB6ywF77IUvA8eFixYBgsWrNfWAZPltufdad+1AAAAAElFTkSuQmCC",
+ "CLEAR_BACKGROUND_COLOR": "#212732",
+ "NODE_TITLE_COLOR": "#999",
+ "NODE_SELECTED_TITLE_COLOR": "#e5eaf0",
+ "NODE_TEXT_SIZE": 14,
+ "NODE_TEXT_COLOR": "#bcc2c8",
+ "NODE_SUBTEXT_SIZE": 12,
+ "NODE_DEFAULT_COLOR": "#2e3440",
+ "NODE_DEFAULT_BGCOLOR": "#161b22",
+ "NODE_DEFAULT_BOXCOLOR": "#545d70",
+ "NODE_DEFAULT_SHAPE": "box",
+ "NODE_BOX_OUTLINE_COLOR": "#e5eaf0",
+ "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
+ "DEFAULT_GROUP_FONT": 24,
+ "WIDGET_BGCOLOR": "#2e3440",
+ "WIDGET_OUTLINE_COLOR": "#545d70",
+ "WIDGET_TEXT_COLOR": "#bcc2c8",
+ "WIDGET_SECONDARY_TEXT_COLOR": "#999",
+ "LINK_COLOR": "#9A9",
+ "EVENT_LINK_COLOR": "#A86",
+ "CONNECTING_LINK_COLOR": "#AFA"
+ },
+ "comfy_base": {
+ "fg-color": "#e5eaf0",
+ "bg-color": "#2e3440",
+ "comfy-menu-bg": "#161b22",
+ "comfy-input-bg": "#2e3440",
+ "input-text": "#bcc2c8",
+ "descrip-text": "#999",
+ "drag-text": "#ccc",
+ "error-text": "#ff4444",
+ "border-color": "#545d70",
+ "tr-even-bg-color": "#2e3440",
+ "tr-odd-bg-color": "#161b22"
+ }
+ },
+ },
+ "github": {
+ "id": "github",
+ "name": "Github",
+ "colors": {
+ "node_slot": {
+ "BOOLEAN": "",
+ "CLIP": "#eacb8b",
+ "CLIP_VISION": "#A8DADC",
+ "CLIP_VISION_OUTPUT": "#ad7452",
+ "CONDITIONING": "#cf876f",
+ "CONTROL_NET": "#00d78d",
+ "CONTROL_NET_WEIGHTS": "",
+ "FLOAT": "",
+ "GLIGEN": "",
+ "IMAGE": "#80a1c0",
+ "IMAGEUPLOAD": "",
+ "INT": "",
+ "LATENT": "#b38ead",
+ "LATENT_KEYFRAME": "",
+ "MASK": "#a3bd8d",
+ "MODEL": "#8978a7",
+ "SAMPLER": "",
+ "SIGMAS": "",
+ "STRING": "",
+ "STYLE_MODEL": "#C2FFAE",
+ "T2I_ADAPTER_WEIGHTS": "",
+ "TAESD": "#DCC274",
+ "TIMESTEP_KEYFRAME": "",
+ "UPSCALE_MODEL": "",
+ "VAE": "#be616b"
+ },
+ "litegraph_base": {
+ "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==",
+ "CLEAR_BACKGROUND_COLOR": "#040506",
+ "NODE_TITLE_COLOR": "#999",
+ "NODE_SELECTED_TITLE_COLOR": "#e5eaf0",
+ "NODE_TEXT_SIZE": 14,
+ "NODE_TEXT_COLOR": "#bcc2c8",
+ "NODE_SUBTEXT_SIZE": 12,
+ "NODE_DEFAULT_COLOR": "#161b22",
+ "NODE_DEFAULT_BGCOLOR": "#13171d",
+ "NODE_DEFAULT_BOXCOLOR": "#30363d",
+ "NODE_DEFAULT_SHAPE": "box",
+ "NODE_BOX_OUTLINE_COLOR": "#e5eaf0",
+ "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
+ "DEFAULT_GROUP_FONT": 24,
+ "WIDGET_BGCOLOR": "#161b22",
+ "WIDGET_OUTLINE_COLOR": "#30363d",
+ "WIDGET_TEXT_COLOR": "#bcc2c8",
+ "WIDGET_SECONDARY_TEXT_COLOR": "#999",
+ "LINK_COLOR": "#9A9",
+ "EVENT_LINK_COLOR": "#A86",
+ "CONNECTING_LINK_COLOR": "#AFA"
+ },
+ "comfy_base": {
+ "fg-color": "#e5eaf0",
+ "bg-color": "#161b22",
+ "comfy-menu-bg": "#13171d",
+ "comfy-input-bg": "#161b22",
+ "input-text": "#bcc2c8",
+ "descrip-text": "#999",
+ "drag-text": "#ccc",
+ "error-text": "#ff4444",
+ "border-color": "#30363d",
+ "tr-even-bg-color": "#161b22",
+ "tr-odd-bg-color": "#13171d"
+ }
+ },
+ }
+};
+
+const id = "Comfy.ColorPalette";
+const idCustomColorPalettes = "Comfy.CustomColorPalettes";
+const defaultColorPaletteId = "dark";
+const els = {}
+// const ctxMenu = LiteGraph.ContextMenu;
+app.registerExtension({
+ name: id,
+ addCustomNodeDefs(node_defs) {
+ const sortObjectKeys = (unordered) => {
+ return Object.keys(unordered).sort().reduce((obj, key) => {
+ obj[key] = unordered[key];
+ return obj;
+ }, {});
+ };
+
+ function getSlotTypes() {
+ var types = [];
+
+ const defs = node_defs;
+ for (const nodeId in defs) {
+ const nodeData = defs[nodeId];
+
+ var inputs = nodeData["input"]["required"];
+ if (nodeData["input"]["optional"] !== undefined) {
+ inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
+ }
+
+ for (const inputName in inputs) {
+ const inputData = inputs[inputName];
+ const type = inputData[0];
+
+ if (!Array.isArray(type)) {
+ types.push(type);
+ }
+ }
+
+ for (const o in nodeData["output"]) {
+ const output = nodeData["output"][o];
+ types.push(output);
+ }
+ }
+
+ return types;
+ }
+
+ function completeColorPalette(colorPalette) {
+ var types = getSlotTypes();
+
+ for (const type of types) {
+ if (!colorPalette.colors.node_slot[type]) {
+ colorPalette.colors.node_slot[type] = "";
+ }
+ }
+
+ colorPalette.colors.node_slot = sortObjectKeys(colorPalette.colors.node_slot);
+
+ return colorPalette;
+ }
+
+ const getColorPaletteTemplate = async () => {
+ let colorPalette = {
+ "id": "my_color_palette_unique_id",
+ "name": "My Color Palette",
+ "colors": {
+ "node_slot": {},
+ "litegraph_base": {},
+ "comfy_base": {}
+ }
+ };
+
+ // Copy over missing keys from default color palette
+ const defaultColorPalette = colorPalettes[defaultColorPaletteId];
+ for (const key in defaultColorPalette.colors.litegraph_base) {
+ if (!colorPalette.colors.litegraph_base[key]) {
+ colorPalette.colors.litegraph_base[key] = "";
+ }
+ }
+ for (const key in defaultColorPalette.colors.comfy_base) {
+ if (!colorPalette.colors.comfy_base[key]) {
+ colorPalette.colors.comfy_base[key] = "";
+ }
+ }
+
+ return completeColorPalette(colorPalette);
+ };
+
+ const getCustomColorPalettes = () => {
+ return app.ui.settings.getSettingValue(idCustomColorPalettes, {});
+ };
+
+ const setCustomColorPalettes = (customColorPalettes) => {
+ return app.ui.settings.setSettingValue(idCustomColorPalettes, customColorPalettes);
+ };
+
+ const addCustomColorPalette = async (colorPalette) => {
+ if (typeof (colorPalette) !== "object") {
+ alert("Invalid color palette.");
+ return;
+ }
+
+ if (!colorPalette.id) {
+ alert("Color palette missing id.");
+ return;
+ }
+
+ if (!colorPalette.name) {
+ alert("Color palette missing name.");
+ return;
+ }
+
+ if (!colorPalette.colors) {
+ alert("Color palette missing colors.");
+ return;
+ }
+
+ if (colorPalette.colors.node_slot && typeof (colorPalette.colors.node_slot) !== "object") {
+ alert("Invalid color palette colors.node_slot.");
+ return;
+ }
+
+ const customColorPalettes = getCustomColorPalettes();
+ customColorPalettes[colorPalette.id] = colorPalette;
+ setCustomColorPalettes(customColorPalettes);
+
+ for (const option of els.select.childNodes) {
+ if (option.value === "custom_" + colorPalette.id) {
+ els.select.removeChild(option);
+ }
+ }
+
+ els.select.append($el("option", {
+ textContent: colorPalette.name + " (custom)",
+ value: "custom_" + colorPalette.id,
+ selected: true
+ }));
+
+ setColorPalette("custom_" + colorPalette.id);
+ await loadColorPalette(colorPalette);
+ };
+
+ const deleteCustomColorPalette = async (colorPaletteId) => {
+ const customColorPalettes = getCustomColorPalettes();
+ delete customColorPalettes[colorPaletteId];
+ setCustomColorPalettes(customColorPalettes);
+
+ for (const option of els.select.childNodes) {
+ if (option.value === defaultColorPaletteId) {
+ option.selected = true;
+ }
+
+ if (option.value === "custom_" + colorPaletteId) {
+ els.select.removeChild(option);
+ }
+ }
+
+ setColorPalette(defaultColorPaletteId);
+ await loadColorPalette(getColorPalette());
+ };
+
+ const loadColorPalette = async (colorPalette) => {
+ colorPalette = await completeColorPalette(colorPalette);
+ if (colorPalette.colors) {
+ // Sets the colors of node slots and links
+ if (colorPalette.colors.node_slot) {
+ Object.assign(app.canvas.default_connection_color_byType, colorPalette.colors.node_slot);
+ Object.assign(LGraphCanvas.link_type_colors, colorPalette.colors.node_slot);
+ }
+ // Sets the colors of the LiteGraph objects
+ if (colorPalette.colors.litegraph_base) {
+ // Everything updates correctly in the loop, except the Node Title and Link Color for some reason
+ app.canvas.node_title_color = colorPalette.colors.litegraph_base.NODE_TITLE_COLOR;
+ app.canvas.default_link_color = colorPalette.colors.litegraph_base.LINK_COLOR;
+
+ for (const key in colorPalette.colors.litegraph_base) {
+ if (colorPalette.colors.litegraph_base.hasOwnProperty(key) && LiteGraph.hasOwnProperty(key)) {
+ LiteGraph[key] = colorPalette.colors.litegraph_base[key];
+ }
+ }
+ }
+ // Sets the color of ComfyUI elements
+ if (colorPalette.colors.comfy_base) {
+ const rootStyle = document.documentElement.style;
+ for (const key in colorPalette.colors.comfy_base) {
+ rootStyle.setProperty('--' + key, colorPalette.colors.comfy_base[key]);
+ }
+ }
+ app.canvas.draw(true, true);
+ }
+ };
+
+ const getColorPalette = (colorPaletteId) => {
+ if (!colorPaletteId) {
+ colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
+ }
+
+ if (colorPaletteId.startsWith("custom_")) {
+ colorPaletteId = colorPaletteId.substr(7);
+ let customColorPalettes = getCustomColorPalettes();
+ if (customColorPalettes[colorPaletteId]) {
+ return customColorPalettes[colorPaletteId];
+ }
+ }
+
+ return colorPalettes[colorPaletteId];
+ };
+
+ const setColorPalette = (colorPaletteId) => {
+ app.ui.settings.setSettingValue(id, colorPaletteId);
+ };
+
+ const fileInput = $el("input", {
+ type: "file",
+ accept: ".json",
+ style: {display: "none"},
+ parent: document.body,
+ onchange: () => {
+ const file = fileInput.files[0];
+ if (file.type === "application/json" || file.name.endsWith(".json")) {
+ const reader = new FileReader();
+ reader.onload = async () => {
+ await addCustomColorPalette(JSON.parse(reader.result));
+ };
+ reader.readAsText(file);
+ }
+ },
+ });
+
+ app.ui.settings.addSetting({
+ id,
+ name: "Color Palette",
+ type: (name, setter, value) => {
+ const options = [
+ ...Object.values(colorPalettes).map(c=> $el("option", {
+ textContent: c.name,
+ value: c.id,
+ selected: c.id === value
+ })),
+ ...Object.values(getCustomColorPalettes()).map(c=>$el("option", {
+ textContent: `${c.name} (custom)`,
+ value: `custom_${c.id}`,
+ selected: `custom_${c.id}` === value
+ })) ,
+ ];
+
+ els.select = $el("select", {
+ style: {
+ marginBottom: "0.15rem",
+ width: "100%",
+ },
+ onchange: (e) => {
+ setter(e.target.value);
+ }
+ }, options)
+
+ return $el("tr", [
+ $el("td", [
+ $el("label", {
+ for: id.replaceAll(".", "-"),
+ textContent: "Color palette",
+ }),
+ ]),
+ $el("td", [
+ els.select,
+ $el("div", {
+ style: {
+ display: "grid",
+ gap: "4px",
+ gridAutoFlow: "column",
+ },
+ }, [
+ $el("input", {
+ type: "button",
+ value: "Export",
+ onclick: async () => {
+ const colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
+ const colorPalette = await completeColorPalette(getColorPalette(colorPaletteId));
+ const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string
+ const blob = new Blob([json], {type: "application/json"});
+ const url = URL.createObjectURL(blob);
+ const a = $el("a", {
+ href: url,
+ download: colorPaletteId + ".json",
+ style: {display: "none"},
+ parent: document.body,
+ });
+ a.click();
+ setTimeout(function () {
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ }, 0);
+ },
+ }),
+ $el("input", {
+ type: "button",
+ value: "Import",
+ onclick: () => {
+ fileInput.click();
+ }
+ }),
+ $el("input", {
+ type: "button",
+ value: "Template",
+ onclick: async () => {
+ const colorPalette = await getColorPaletteTemplate();
+ const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string
+ const blob = new Blob([json], {type: "application/json"});
+ const url = URL.createObjectURL(blob);
+ const a = $el("a", {
+ href: url,
+ download: "color_palette.json",
+ style: {display: "none"},
+ parent: document.body,
+ });
+ a.click();
+ setTimeout(function () {
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ }, 0);
+ }
+ }),
+ $el("input", {
+ type: "button",
+ value: "Delete",
+ onclick: async () => {
+ let colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
+
+ if (colorPalettes[colorPaletteId]) {
+ alert("You cannot delete a built-in color palette.");
+ return;
+ }
+
+ if (colorPaletteId.startsWith("custom_")) {
+ colorPaletteId = colorPaletteId.substr(7);
+ }
+
+ await deleteCustomColorPalette(colorPaletteId);
+ }
+ }),
+ ]),
+ ]),
+ ])
+ },
+ defaultValue: defaultColorPaletteId,
+ async onChange(value) {
+ if (!value) {
+ return;
+ }
+
+ let palette = colorPalettes[value];
+ if (palette) {
+ await loadColorPalette(palette);
+ } else if (value.startsWith("custom_")) {
+ value = value.substr(7);
+ let customColorPalettes = getCustomColorPalettes();
+ if (customColorPalettes[value]) {
+ palette = customColorPalettes[value];
+ await loadColorPalette(customColorPalettes[value]);
+ }
+ }
+
+ let {BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR} = palette.colors.litegraph_base;
+ if (BACKGROUND_IMAGE === undefined || CLEAR_BACKGROUND_COLOR === undefined) {
+ const base = colorPalettes["dark"].colors.litegraph_base;
+ BACKGROUND_IMAGE = base.BACKGROUND_IMAGE;
+ CLEAR_BACKGROUND_COLOR = base.CLEAR_BACKGROUND_COLOR;
+ }
+ app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR);
+ },
+ });
+ },
+});
diff --git a/ComfyUI/web/extensions/core/contextMenuFilter.js b/ComfyUI/web/extensions/core/contextMenuFilter.js
new file mode 100644
index 0000000000000000000000000000000000000000..0a305391a4e11fce506fd8cc082ad9c7eaa71f2b
--- /dev/null
+++ b/ComfyUI/web/extensions/core/contextMenuFilter.js
@@ -0,0 +1,148 @@
+import {app} from "../../scripts/app.js";
+
+// Adds filtering to combo context menus
+
+const ext = {
+ name: "Comfy.ContextMenuFilter",
+ init() {
+ const ctxMenu = LiteGraph.ContextMenu;
+
+ LiteGraph.ContextMenu = function (values, options) {
+ const ctx = ctxMenu.call(this, values, options);
+
+ // If we are a dark menu (only used for combo boxes) then add a filter input
+ if (options?.className === "dark" && values?.length > 10) {
+ const filter = document.createElement("input");
+ filter.classList.add("comfy-context-menu-filter");
+ filter.placeholder = "Filter list";
+ this.root.prepend(filter);
+
+ const items = Array.from(this.root.querySelectorAll(".litemenu-entry"));
+ let displayedItems = [...items];
+ let itemCount = displayedItems.length;
+
+ // We must request an animation frame for the current node of the active canvas to update.
+ requestAnimationFrame(() => {
+ const currentNode = LGraphCanvas.active_canvas.current_node;
+ const clickedComboValue = currentNode.widgets
+ ?.filter(w => w.type === "combo" && w.options.values.length === values.length)
+ .find(w => w.options.values.every((v, i) => v === values[i]))
+ ?.value;
+
+ let selectedIndex = clickedComboValue ? values.findIndex(v => v === clickedComboValue) : 0;
+ if (selectedIndex < 0) {
+ selectedIndex = 0;
+ }
+ let selectedItem = displayedItems[selectedIndex];
+ updateSelected();
+
+ // Apply highlighting to the selected item
+ function updateSelected() {
+ selectedItem?.style.setProperty("background-color", "");
+ selectedItem?.style.setProperty("color", "");
+ selectedItem = displayedItems[selectedIndex];
+ selectedItem?.style.setProperty("background-color", "#ccc", "important");
+ selectedItem?.style.setProperty("color", "#000", "important");
+ }
+
+ const positionList = () => {
+ const rect = this.root.getBoundingClientRect();
+
+ // If the top is off-screen then shift the element with scaling applied
+ if (rect.top < 0) {
+ const scale = 1 - this.root.getBoundingClientRect().height / this.root.clientHeight;
+ const shift = (this.root.clientHeight * scale) / 2;
+ this.root.style.top = -shift + "px";
+ }
+ }
+
+ // Arrow up/down to select items
+ filter.addEventListener("keydown", (event) => {
+ switch (event.key) {
+ case "ArrowUp":
+ event.preventDefault();
+ if (selectedIndex === 0) {
+ selectedIndex = itemCount - 1;
+ } else {
+ selectedIndex--;
+ }
+ updateSelected();
+ break;
+ case "ArrowRight":
+ event.preventDefault();
+ selectedIndex = itemCount - 1;
+ updateSelected();
+ break;
+ case "ArrowDown":
+ event.preventDefault();
+ if (selectedIndex === itemCount - 1) {
+ selectedIndex = 0;
+ } else {
+ selectedIndex++;
+ }
+ updateSelected();
+ break;
+ case "ArrowLeft":
+ event.preventDefault();
+ selectedIndex = 0;
+ updateSelected();
+ break;
+ case "Enter":
+ selectedItem?.click();
+ break;
+ case "Escape":
+ this.close();
+ break;
+ }
+ });
+
+ filter.addEventListener("input", () => {
+ // Hide all items that don't match our filter
+ const term = filter.value.toLocaleLowerCase();
+ // When filtering, recompute which items are visible for arrow up/down and maintain selection.
+ displayedItems = items.filter(item => {
+ const isVisible = !term || item.textContent.toLocaleLowerCase().includes(term);
+ item.style.display = isVisible ? "block" : "none";
+ return isVisible;
+ });
+
+ selectedIndex = 0;
+ if (displayedItems.includes(selectedItem)) {
+ selectedIndex = displayedItems.findIndex(d => d === selectedItem);
+ }
+ itemCount = displayedItems.length;
+
+ updateSelected();
+
+ // If we have an event then we can try and position the list under the source
+ if (options.event) {
+ let top = options.event.clientY - 10;
+
+ const bodyRect = document.body.getBoundingClientRect();
+ const rootRect = this.root.getBoundingClientRect();
+ if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) {
+ top = Math.max(0, bodyRect.height - rootRect.height - 10);
+ }
+
+ this.root.style.top = top + "px";
+ positionList();
+ }
+ });
+
+ requestAnimationFrame(() => {
+ // Focus the filter box when opening
+ filter.focus();
+
+ positionList();
+ });
+ })
+ }
+
+ return ctx;
+ };
+
+ LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
+ },
+}
+
+app.registerExtension(ext);
diff --git a/ComfyUI/web/extensions/core/dynamicPrompts.js b/ComfyUI/web/extensions/core/dynamicPrompts.js
new file mode 100644
index 0000000000000000000000000000000000000000..599a9e685893dafbcdebb149bdfb68ab85db142d
--- /dev/null
+++ b/ComfyUI/web/extensions/core/dynamicPrompts.js
@@ -0,0 +1,48 @@
+import { app } from "../../scripts/app.js";
+
+// Allows for simple dynamic prompt replacement
+// Inputs in the format {a|b} will have a random value of a or b chosen when the prompt is queued.
+
+/*
+ * Strips C-style line and block comments from a string
+ */
+function stripComments(str) {
+ return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,'');
+}
+
+app.registerExtension({
+ name: "Comfy.DynamicPrompts",
+ nodeCreated(node) {
+ if (node.widgets) {
+ // Locate dynamic prompt text widgets
+ // Include any widgets with dynamicPrompts set to true, and customtext
+ const widgets = node.widgets.filter(
+ (n) => (n.type === "customtext" && n.dynamicPrompts !== false) || n.dynamicPrompts
+ );
+ for (const widget of widgets) {
+ // Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node
+ widget.serializeValue = (workflowNode, widgetIndex) => {
+ let prompt = stripComments(widget.value);
+ while (prompt.replace("\\{", "").includes("{") && prompt.replace("\\}", "").includes("}")) {
+ const startIndex = prompt.replace("\\{", "00").indexOf("{");
+ const endIndex = prompt.replace("\\}", "00").indexOf("}");
+
+ const optionsString = prompt.substring(startIndex + 1, endIndex);
+ const options = optionsString.split("|");
+
+ const randomIndex = Math.floor(Math.random() * options.length);
+ const randomOption = options[randomIndex];
+
+ prompt = prompt.substring(0, startIndex) + randomOption + prompt.substring(endIndex + 1);
+ }
+
+ // Overwrite the value in the serialized workflow pnginfo
+ if (workflowNode?.widgets_values)
+ workflowNode.widgets_values[widgetIndex] = prompt;
+
+ return prompt;
+ };
+ }
+ }
+ },
+});
diff --git a/ComfyUI/web/extensions/core/editAttention.js b/ComfyUI/web/extensions/core/editAttention.js
new file mode 100644
index 0000000000000000000000000000000000000000..6792b235720115c4bc5c29694b25c4a66cd4a3bf
--- /dev/null
+++ b/ComfyUI/web/extensions/core/editAttention.js
@@ -0,0 +1,144 @@
+import { app } from "../../scripts/app.js";
+
+// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
+
+app.registerExtension({
+ name: "Comfy.EditAttention",
+ init() {
+ const editAttentionDelta = app.ui.settings.addSetting({
+ id: "Comfy.EditAttention.Delta",
+ name: "Ctrl+up/down precision",
+ type: "slider",
+ attrs: {
+ min: 0.01,
+ max: 0.5,
+ step: 0.01,
+ },
+ defaultValue: 0.05,
+ });
+
+ function incrementWeight(weight, delta) {
+ const floatWeight = parseFloat(weight);
+ if (isNaN(floatWeight)) return weight;
+ const newWeight = floatWeight + delta;
+ if (newWeight < 0) return "0";
+ return String(Number(newWeight.toFixed(10)));
+ }
+
+ function findNearestEnclosure(text, cursorPos) {
+ let start = cursorPos, end = cursorPos;
+ let openCount = 0, closeCount = 0;
+
+ // Find opening parenthesis before cursor
+ while (start >= 0) {
+ start--;
+ if (text[start] === "(" && openCount === closeCount) break;
+ if (text[start] === "(") openCount++;
+ if (text[start] === ")") closeCount++;
+ }
+ if (start < 0) return false;
+
+ openCount = 0;
+ closeCount = 0;
+
+ // Find closing parenthesis after cursor
+ while (end < text.length) {
+ if (text[end] === ")" && openCount === closeCount) break;
+ if (text[end] === "(") openCount++;
+ if (text[end] === ")") closeCount++;
+ end++;
+ }
+ if (end === text.length) return false;
+
+ return { start: start + 1, end: end };
+ }
+
+ function addWeightToParentheses(text) {
+ const parenRegex = /^\((.*)\)$/;
+ const parenMatch = text.match(parenRegex);
+
+ const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/;
+ const floatMatch = text.match(floatRegex);
+
+ if (parenMatch && !floatMatch) {
+ return `(${parenMatch[1]}:1.0)`;
+ } else {
+ return text;
+ }
+ };
+
+ function editAttention(event) {
+ const inputField = event.composedPath()[0];
+ const delta = parseFloat(editAttentionDelta.value);
+
+ if (inputField.tagName !== "TEXTAREA") return;
+ if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
+ if (!event.ctrlKey && !event.metaKey) return;
+
+ event.preventDefault();
+
+ let start = inputField.selectionStart;
+ let end = inputField.selectionEnd;
+ let selectedText = inputField.value.substring(start, end);
+
+ // If there is no selection, attempt to find the nearest enclosure, or select the current word
+ if (!selectedText) {
+ const nearestEnclosure = findNearestEnclosure(inputField.value, start);
+ if (nearestEnclosure) {
+ start = nearestEnclosure.start;
+ end = nearestEnclosure.end;
+ selectedText = inputField.value.substring(start, end);
+ } else {
+ // Select the current word, find the start and end of the word
+ const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t";
+
+ while (!delimiters.includes(inputField.value[start - 1]) && start > 0) {
+ start--;
+ }
+
+ while (!delimiters.includes(inputField.value[end]) && end < inputField.value.length) {
+ end++;
+ }
+
+ selectedText = inputField.value.substring(start, end);
+ if (!selectedText) return;
+ }
+ }
+
+ // If the selection ends with a space, remove it
+ if (selectedText[selectedText.length - 1] === " ") {
+ selectedText = selectedText.substring(0, selectedText.length - 1);
+ end -= 1;
+ }
+
+ // If there are parentheses left and right of the selection, select them
+ if (inputField.value[start - 1] === "(" && inputField.value[end] === ")") {
+ start -= 1;
+ end += 1;
+ selectedText = inputField.value.substring(start, end);
+ }
+
+ // If the selection is not enclosed in parentheses, add them
+ if (selectedText[0] !== "(" || selectedText[selectedText.length - 1] !== ")") {
+ selectedText = `(${selectedText})`;
+ }
+
+ // If the selection does not have a weight, add a weight of 1.0
+ selectedText = addWeightToParentheses(selectedText);
+
+ // Increment the weight
+ const weightDelta = event.key === "ArrowUp" ? delta : -delta;
+ const updatedText = selectedText.replace(/\((.*):(\d+(?:\.\d+)?)\)/, (match, text, weight) => {
+ weight = incrementWeight(weight, weightDelta);
+ if (weight == 1) {
+ return text;
+ } else {
+ return `(${text}:${weight})`;
+ }
+ });
+
+ inputField.setRangeText(updatedText, start, end, "select");
+ }
+ window.addEventListener("keydown", editAttention);
+ },
+});
diff --git a/ComfyUI/web/extensions/core/groupNode.js b/ComfyUI/web/extensions/core/groupNode.js
new file mode 100644
index 0000000000000000000000000000000000000000..4cf1f7621b9a7626abcf6d0f1617963cda17fc32
--- /dev/null
+++ b/ComfyUI/web/extensions/core/groupNode.js
@@ -0,0 +1,1172 @@
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js";
+import { mergeIfValid } from "./widgetInputs.js";
+
+const GROUP = Symbol();
+
+const Workflow = {
+ InUse: {
+ Free: 0,
+ Registered: 1,
+ InWorkflow: 2,
+ },
+ isInUseGroupNode(name) {
+ const id = `workflow/${name}`;
+ // Check if lready registered/in use in this workflow
+ if (app.graph.extra?.groupNodes?.[name]) {
+ if (app.graph._nodes.find((n) => n.type === id)) {
+ return Workflow.InUse.InWorkflow;
+ } else {
+ return Workflow.InUse.Registered;
+ }
+ }
+ return Workflow.InUse.Free;
+ },
+ storeGroupNode(name, data) {
+ let extra = app.graph.extra;
+ if (!extra) app.graph.extra = extra = {};
+ let groupNodes = extra.groupNodes;
+ if (!groupNodes) extra.groupNodes = groupNodes = {};
+ groupNodes[name] = data;
+ },
+};
+
+class GroupNodeBuilder {
+ constructor(nodes) {
+ this.nodes = nodes;
+ }
+
+ build() {
+ const name = this.getName();
+ if (!name) return;
+
+ // Sort the nodes so they are in execution order
+ // this allows for widgets to be in the correct order when reconstructing
+ this.sortNodes();
+
+ this.nodeData = this.getNodeData();
+ Workflow.storeGroupNode(name, this.nodeData);
+
+ return { name, nodeData: this.nodeData };
+ }
+
+ getName() {
+ const name = prompt("Enter group name");
+ if (!name) return;
+ const used = Workflow.isInUseGroupNode(name);
+ switch (used) {
+ case Workflow.InUse.InWorkflow:
+ alert(
+ "An in use group node with this name already exists embedded in this workflow, please remove any instances or use a new name."
+ );
+ return;
+ case Workflow.InUse.Registered:
+ if (
+ !confirm(
+ "An group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?"
+ )
+ ) {
+ return;
+ }
+ break;
+ }
+ return name;
+ }
+
+ sortNodes() {
+ // Gets the builders nodes in graph execution order
+ const nodesInOrder = app.graph.computeExecutionOrder(false);
+ this.nodes = this.nodes
+ .map((node) => ({ index: nodesInOrder.indexOf(node), node }))
+ .sort((a, b) => a.index - b.index || a.node.id - b.node.id)
+ .map(({ node }) => node);
+ }
+
+ getNodeData() {
+ const storeLinkTypes = (config) => {
+ // Store link types for dynamically typed nodes e.g. reroutes
+ for (const link of config.links) {
+ const origin = app.graph.getNodeById(link[4]);
+ const type = origin.outputs[link[1]].type;
+ link.push(type);
+ }
+ };
+
+ const storeExternalLinks = (config) => {
+ // Store any external links to the group in the config so when rebuilding we add extra slots
+ config.external = [];
+ for (let i = 0; i < this.nodes.length; i++) {
+ const node = this.nodes[i];
+ if (!node.outputs?.length) continue;
+ for (let slot = 0; slot < node.outputs.length; slot++) {
+ let hasExternal = false;
+ const output = node.outputs[slot];
+ let type = output.type;
+ if (!output.links?.length) continue;
+ for (const l of output.links) {
+ const link = app.graph.links[l];
+ if (!link) continue;
+ if (type === "*") type = link.type;
+
+ if (!app.canvas.selected_nodes[link.target_id]) {
+ hasExternal = true;
+ break;
+ }
+ }
+ if (hasExternal) {
+ config.external.push([i, slot, type]);
+ }
+ }
+ }
+ };
+
+ // Use the built in copyToClipboard function to generate the node data we need
+ const backup = localStorage.getItem("litegrapheditor_clipboard");
+ try {
+ app.canvas.copyToClipboard(this.nodes);
+ const config = JSON.parse(localStorage.getItem("litegrapheditor_clipboard"));
+
+ storeLinkTypes(config);
+ storeExternalLinks(config);
+
+ return config;
+ } finally {
+ localStorage.setItem("litegrapheditor_clipboard", backup);
+ }
+ }
+}
+
+export class GroupNodeConfig {
+ constructor(name, nodeData) {
+ this.name = name;
+ this.nodeData = nodeData;
+ this.getLinks();
+
+ this.inputCount = 0;
+ this.oldToNewOutputMap = {};
+ this.newToOldOutputMap = {};
+ this.oldToNewInputMap = {};
+ this.oldToNewWidgetMap = {};
+ this.newToOldWidgetMap = {};
+ this.primitiveDefs = {};
+ this.widgetToPrimitive = {};
+ this.primitiveToWidget = {};
+ }
+
+ async registerType(source = "workflow") {
+ this.nodeDef = {
+ output: [],
+ output_name: [],
+ output_is_list: [],
+ name: source + "/" + this.name,
+ display_name: this.name,
+ category: "group nodes" + ("/" + source),
+ input: { required: {} },
+
+ [GROUP]: this,
+ };
+
+ this.inputs = [];
+ const seenInputs = {};
+ const seenOutputs = {};
+ for (let i = 0; i < this.nodeData.nodes.length; i++) {
+ const node = this.nodeData.nodes[i];
+ node.index = i;
+ this.processNode(node, seenInputs, seenOutputs);
+ }
+
+ for (const p of this.#convertedToProcess) {
+ p();
+ }
+ this.#convertedToProcess = null;
+ await app.registerNodeDef("workflow/" + this.name, this.nodeDef);
+ }
+
+ getLinks() {
+ this.linksFrom = {};
+ this.linksTo = {};
+ this.externalFrom = {};
+
+ // Extract links for easy lookup
+ for (const l of this.nodeData.links) {
+ const [sourceNodeId, sourceNodeSlot, targetNodeId, targetNodeSlot] = l;
+
+ // Skip links outside the copy config
+ if (sourceNodeId == null) continue;
+
+ if (!this.linksFrom[sourceNodeId]) {
+ this.linksFrom[sourceNodeId] = {};
+ }
+ if (!this.linksFrom[sourceNodeId][sourceNodeSlot]) {
+ this.linksFrom[sourceNodeId][sourceNodeSlot] = [];
+ }
+ this.linksFrom[sourceNodeId][sourceNodeSlot].push(l);
+
+ if (!this.linksTo[targetNodeId]) {
+ this.linksTo[targetNodeId] = {};
+ }
+ this.linksTo[targetNodeId][targetNodeSlot] = l;
+ }
+
+ if (this.nodeData.external) {
+ for (const ext of this.nodeData.external) {
+ if (!this.externalFrom[ext[0]]) {
+ this.externalFrom[ext[0]] = { [ext[1]]: ext[2] };
+ } else {
+ this.externalFrom[ext[0]][ext[1]] = ext[2];
+ }
+ }
+ }
+ }
+
+ processNode(node, seenInputs, seenOutputs) {
+ const def = this.getNodeDef(node);
+ if (!def) return;
+
+ const inputs = { ...def.input?.required, ...def.input?.optional };
+
+ this.inputs.push(this.processNodeInputs(node, seenInputs, inputs));
+ if (def.output?.length) this.processNodeOutputs(node, seenOutputs, def);
+ }
+
+ getNodeDef(node) {
+ const def = globalDefs[node.type];
+ if (def) return def;
+
+ const linksFrom = this.linksFrom[node.index];
+ if (node.type === "PrimitiveNode") {
+ // Skip as its not linked
+ if (!linksFrom) return;
+
+ let type = linksFrom["0"][0][5];
+ if (type === "COMBO") {
+ // Use the array items
+ const source = node.outputs[0].widget.name;
+ const fromTypeName = this.nodeData.nodes[linksFrom["0"][0][2]].type;
+ const fromType = globalDefs[fromTypeName];
+ const input = fromType.input.required[source] ?? fromType.input.optional[source];
+ type = input[0];
+ }
+
+ const def = (this.primitiveDefs[node.index] = {
+ input: {
+ required: {
+ value: [type, {}],
+ },
+ },
+ output: [type],
+ output_name: [],
+ output_is_list: [],
+ });
+ return def;
+ } else if (node.type === "Reroute") {
+ const linksTo = this.linksTo[node.index];
+ if (linksTo && linksFrom && !this.externalFrom[node.index]?.[0]) {
+ // Being used internally
+ return null;
+ }
+
+ let config = {};
+ let rerouteType = "*";
+ if (linksFrom) {
+ for (const [, , id, slot] of linksFrom["0"]) {
+ const node = this.nodeData.nodes[id];
+ const input = node.inputs[slot];
+ if (rerouteType === "*") {
+ rerouteType = input.type;
+ }
+ if (input.widget) {
+ const targetDef = globalDefs[node.type];
+ const targetWidget =
+ targetDef.input.required[input.widget.name] ?? targetDef.input.optional[input.widget.name];
+
+ const widget = [targetWidget[0], config];
+ const res = mergeIfValid(
+ {
+ widget,
+ },
+ targetWidget,
+ false,
+ null,
+ widget
+ );
+ config = res?.customConfig ?? config;
+ }
+ }
+ } else if (linksTo) {
+ const [id, slot] = linksTo["0"];
+ rerouteType = this.nodeData.nodes[id].outputs[slot].type;
+ } else {
+ // Reroute used as a pipe
+ for (const l of this.nodeData.links) {
+ if (l[2] === node.index) {
+ rerouteType = l[5];
+ break;
+ }
+ }
+ if (rerouteType === "*") {
+ // Check for an external link
+ const t = this.externalFrom[node.index]?.[0];
+ if (t) {
+ rerouteType = t;
+ }
+ }
+ }
+
+ config.forceInput = true;
+ return {
+ input: {
+ required: {
+ [rerouteType]: [rerouteType, config],
+ },
+ },
+ output: [rerouteType],
+ output_name: [],
+ output_is_list: [],
+ };
+ }
+
+ console.warn("Skipping virtual node " + node.type + " when building group node " + this.name);
+ }
+
+ getInputConfig(node, inputName, seenInputs, config, extra) {
+ let name = node.inputs?.find((inp) => inp.name === inputName)?.label ?? inputName;
+ let key = name;
+ let prefix = "";
+ // Special handling for primitive to include the title if it is set rather than just "value"
+ if ((node.type === "PrimitiveNode" && node.title) || name in seenInputs) {
+ prefix = `${node.title ?? node.type} `;
+ key = name = `${prefix}${inputName}`;
+ if (name in seenInputs) {
+ name = `${prefix}${seenInputs[name]} ${inputName}`;
+ }
+ }
+ seenInputs[key] = (seenInputs[key] ?? 1) + 1;
+
+ if (inputName === "seed" || inputName === "noise_seed") {
+ if (!extra) extra = {};
+ extra.control_after_generate = `${prefix}control_after_generate`;
+ }
+ if (config[0] === "IMAGEUPLOAD") {
+ if (!extra) extra = {};
+ extra.widget = `${prefix}${config[1]?.widget ?? "image"}`;
+ }
+
+ if (extra) {
+ config = [config[0], { ...config[1], ...extra }];
+ }
+
+ return { name, config };
+ }
+
+ processWidgetInputs(inputs, node, inputNames, seenInputs) {
+ const slots = [];
+ const converted = new Map();
+ const widgetMap = (this.oldToNewWidgetMap[node.index] = {});
+ for (const inputName of inputNames) {
+ let widgetType = app.getWidgetType(inputs[inputName], inputName);
+ if (widgetType) {
+ const convertedIndex = node.inputs?.findIndex(
+ (inp) => inp.name === inputName && inp.widget?.name === inputName
+ );
+ if (convertedIndex > -1) {
+ // This widget has been converted to a widget
+ // We need to store this in the correct position so link ids line up
+ converted.set(convertedIndex, inputName);
+ widgetMap[inputName] = null;
+ } else {
+ // Normal widget
+ const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]);
+ this.nodeDef.input.required[name] = config;
+ widgetMap[inputName] = name;
+ this.newToOldWidgetMap[name] = { node, inputName };
+ }
+ } else {
+ // Normal input
+ slots.push(inputName);
+ }
+ }
+ return { converted, slots };
+ }
+
+ checkPrimitiveConnection(link, inputName, inputs) {
+ const sourceNode = this.nodeData.nodes[link[0]];
+ if (sourceNode.type === "PrimitiveNode") {
+ // Merge link configurations
+ const [sourceNodeId, _, targetNodeId, __] = link;
+ const primitiveDef = this.primitiveDefs[sourceNodeId];
+ const targetWidget = inputs[inputName];
+ const primitiveConfig = primitiveDef.input.required.value;
+ const output = { widget: primitiveConfig };
+ const config = mergeIfValid(output, targetWidget, false, null, primitiveConfig);
+ primitiveConfig[1] = config?.customConfig ?? inputs[inputName][1] ? { ...inputs[inputName][1] } : {};
+
+ let name = this.oldToNewWidgetMap[sourceNodeId]["value"];
+ name = name.substr(0, name.length - 6);
+ primitiveConfig[1].control_after_generate = true;
+ primitiveConfig[1].control_prefix = name;
+
+ let toPrimitive = this.widgetToPrimitive[targetNodeId];
+ if (!toPrimitive) {
+ toPrimitive = this.widgetToPrimitive[targetNodeId] = {};
+ }
+ if (toPrimitive[inputName]) {
+ toPrimitive[inputName].push(sourceNodeId);
+ }
+ toPrimitive[inputName] = sourceNodeId;
+
+ let toWidget = this.primitiveToWidget[sourceNodeId];
+ if (!toWidget) {
+ toWidget = this.primitiveToWidget[sourceNodeId] = [];
+ }
+ toWidget.push({ nodeId: targetNodeId, inputName });
+ }
+ }
+
+ processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs) {
+ for (let i = 0; i < slots.length; i++) {
+ const inputName = slots[i];
+ if (linksTo[i]) {
+ this.checkPrimitiveConnection(linksTo[i], inputName, inputs);
+ // This input is linked so we can skip it
+ continue;
+ }
+
+ const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]);
+ this.nodeDef.input.required[name] = config;
+ inputMap[i] = this.inputCount++;
+ }
+ }
+
+ processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs) {
+ // Add converted widgets sorted into their index order (ordered as they were converted) so link ids match up
+ const convertedSlots = [...converted.keys()].sort().map((k) => converted.get(k));
+ for (let i = 0; i < convertedSlots.length; i++) {
+ const inputName = convertedSlots[i];
+ if (linksTo[slots.length + i]) {
+ this.checkPrimitiveConnection(linksTo[slots.length + i], inputName, inputs);
+ // This input is linked so we can skip it
+ continue;
+ }
+
+ const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName], {
+ defaultInput: true,
+ });
+ this.nodeDef.input.required[name] = config;
+ this.newToOldWidgetMap[name] = { node, inputName };
+
+ if (!this.oldToNewWidgetMap[node.index]) {
+ this.oldToNewWidgetMap[node.index] = {};
+ }
+ this.oldToNewWidgetMap[node.index][inputName] = name;
+
+ inputMap[slots.length + i] = this.inputCount++;
+ }
+ }
+
+ #convertedToProcess = [];
+ processNodeInputs(node, seenInputs, inputs) {
+ const inputMapping = [];
+
+ const inputNames = Object.keys(inputs);
+ if (!inputNames.length) return;
+
+ const { converted, slots } = this.processWidgetInputs(inputs, node, inputNames, seenInputs);
+ const linksTo = this.linksTo[node.index] ?? {};
+ const inputMap = (this.oldToNewInputMap[node.index] = {});
+ this.processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs);
+
+ // Converted inputs have to be processed after all other nodes as they'll be at the end of the list
+ this.#convertedToProcess.push(() =>
+ this.processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs)
+ );
+
+ return inputMapping;
+ }
+
+ processNodeOutputs(node, seenOutputs, def) {
+ const oldToNew = (this.oldToNewOutputMap[node.index] = {});
+
+ // Add outputs
+ for (let outputId = 0; outputId < def.output.length; outputId++) {
+ const linksFrom = this.linksFrom[node.index];
+ if (linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId]) {
+ // This output is linked internally so we can skip it
+ continue;
+ }
+
+ oldToNew[outputId] = this.nodeDef.output.length;
+ this.newToOldOutputMap[this.nodeDef.output.length] = { node, slot: outputId };
+ this.nodeDef.output.push(def.output[outputId]);
+ this.nodeDef.output_is_list.push(def.output_is_list[outputId]);
+
+ let label = def.output_name?.[outputId] ?? def.output[outputId];
+ const output = node.outputs.find((o) => o.name === label);
+ if (output?.label) {
+ label = output.label;
+ }
+ let name = label;
+ if (name in seenOutputs) {
+ const prefix = `${node.title ?? node.type} `;
+ name = `${prefix}${label}`;
+ if (name in seenOutputs) {
+ name = `${prefix}${node.index} ${label}`;
+ }
+ }
+ seenOutputs[name] = 1;
+
+ this.nodeDef.output_name.push(name);
+ }
+ }
+
+ static async registerFromWorkflow(groupNodes, missingNodeTypes) {
+ const clean = app.clean;
+ app.clean = function () {
+ for (const g in groupNodes) {
+ try {
+ LiteGraph.unregisterNodeType("workflow/" + g);
+ } catch (error) {}
+ }
+ app.clean = clean;
+ };
+
+ for (const g in groupNodes) {
+ const groupData = groupNodes[g];
+
+ let hasMissing = false;
+ for (const n of groupData.nodes) {
+ // Find missing node types
+ if (!(n.type in LiteGraph.registered_node_types)) {
+ missingNodeTypes.push({
+ type: n.type,
+ hint: ` (In group node 'workflow/${g}')`,
+ });
+
+ missingNodeTypes.push({
+ type: "workflow/" + g,
+ action: {
+ text: "Remove from workflow",
+ callback: (e) => {
+ delete groupNodes[g];
+ e.target.textContent = "Removed";
+ e.target.style.pointerEvents = "none";
+ e.target.style.opacity = 0.7;
+ },
+ },
+ });
+
+ hasMissing = true;
+ }
+ }
+
+ if (hasMissing) continue;
+
+ const config = new GroupNodeConfig(g, groupData);
+ await config.registerType();
+ }
+ }
+}
+
+export class GroupNodeHandler {
+ node;
+ groupData;
+
+ constructor(node) {
+ this.node = node;
+ this.groupData = node.constructor?.nodeData?.[GROUP];
+
+ this.node.setInnerNodes = (innerNodes) => {
+ this.innerNodes = innerNodes;
+
+ for (let innerNodeIndex = 0; innerNodeIndex < this.innerNodes.length; innerNodeIndex++) {
+ const innerNode = this.innerNodes[innerNodeIndex];
+
+ for (const w of innerNode.widgets ?? []) {
+ if (w.type === "converted-widget") {
+ w.serializeValue = w.origSerializeValue;
+ }
+ }
+
+ innerNode.index = innerNodeIndex;
+ innerNode.getInputNode = (slot) => {
+ // Check if this input is internal or external
+ const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot];
+ if (externalSlot != null) {
+ return this.node.getInputNode(externalSlot);
+ }
+
+ // Internal link
+ const innerLink = this.groupData.linksTo[innerNode.index]?.[slot];
+ if (!innerLink) return null;
+
+ const inputNode = innerNodes[innerLink[0]];
+ // Primitives will already apply their values
+ if (inputNode.type === "PrimitiveNode") return null;
+
+ return inputNode;
+ };
+
+ innerNode.getInputLink = (slot) => {
+ const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot];
+ if (externalSlot != null) {
+ // The inner node is connected via the group node inputs
+ const linkId = this.node.inputs[externalSlot].link;
+ let link = app.graph.links[linkId];
+
+ // Use the outer link, but update the target to the inner node
+ link = {
+ ...link,
+ target_id: innerNode.id,
+ target_slot: +slot,
+ };
+ return link;
+ }
+
+ let link = this.groupData.linksTo[innerNode.index]?.[slot];
+ if (!link) return null;
+ // Use the inner link, but update the origin node to be inner node id
+ link = {
+ origin_id: innerNodes[link[0]].id,
+ origin_slot: link[1],
+ target_id: innerNode.id,
+ target_slot: +slot,
+ };
+ return link;
+ };
+ }
+ };
+
+ this.node.updateLink = (link) => {
+ // Replace the group node reference with the internal node
+ link = { ...link };
+ const output = this.groupData.newToOldOutputMap[link.origin_slot];
+ let innerNode = this.innerNodes[output.node.index];
+ let l;
+ while (innerNode?.type === "Reroute") {
+ l = innerNode.getInputLink(0);
+ innerNode = innerNode.getInputNode(0);
+ }
+
+ if (!innerNode) {
+ return null;
+ }
+
+ if (l && GroupNodeHandler.isGroupNode(innerNode)) {
+ return innerNode.updateLink(l);
+ }
+
+ link.origin_id = innerNode.id;
+ link.origin_slot = l?.origin_slot ?? output.slot;
+ return link;
+ };
+
+ this.node.getInnerNodes = () => {
+ if (!this.innerNodes) {
+ this.node.setInnerNodes(
+ this.groupData.nodeData.nodes.map((n, i) => {
+ const innerNode = LiteGraph.createNode(n.type);
+ innerNode.configure(n);
+ innerNode.id = `${this.node.id}:${i}`;
+ return innerNode;
+ })
+ );
+ }
+
+ this.updateInnerWidgets();
+
+ return this.innerNodes;
+ };
+
+ this.node.convertToNodes = () => {
+ const addInnerNodes = () => {
+ const backup = localStorage.getItem("litegrapheditor_clipboard");
+ // Clone the node data so we dont mutate it for other nodes
+ const c = { ...this.groupData.nodeData };
+ c.nodes = [...c.nodes];
+ const innerNodes = this.node.getInnerNodes();
+ let ids = [];
+ for (let i = 0; i < c.nodes.length; i++) {
+ let id = innerNodes?.[i]?.id;
+ // Use existing IDs if they are set on the inner nodes
+ if (id == null || isNaN(id)) {
+ id = undefined;
+ } else {
+ ids.push(id);
+ }
+ c.nodes[i] = { ...c.nodes[i], id };
+ }
+ localStorage.setItem("litegrapheditor_clipboard", JSON.stringify(c));
+ app.canvas.pasteFromClipboard();
+ localStorage.setItem("litegrapheditor_clipboard", backup);
+
+ const [x, y] = this.node.pos;
+ let top;
+ let left;
+ // Configure nodes with current widget data
+ const selectedIds = ids.length ? ids : Object.keys(app.canvas.selected_nodes);
+ const newNodes = [];
+ for (let i = 0; i < selectedIds.length; i++) {
+ const id = selectedIds[i];
+ const newNode = app.graph.getNodeById(id);
+ const innerNode = innerNodes[i];
+ newNodes.push(newNode);
+
+ if (left == null || newNode.pos[0] < left) {
+ left = newNode.pos[0];
+ }
+ if (top == null || newNode.pos[1] < top) {
+ top = newNode.pos[1];
+ }
+
+ if (!newNode.widgets) continue;
+
+ const map = this.groupData.oldToNewWidgetMap[innerNode.index];
+ if (map) {
+ const widgets = Object.keys(map);
+
+ for (const oldName of widgets) {
+ const newName = map[oldName];
+ if (!newName) continue;
+
+ const widgetIndex = this.node.widgets.findIndex((w) => w.name === newName);
+ if (widgetIndex === -1) continue;
+
+ // Populate the main and any linked widgets
+ if (innerNode.type === "PrimitiveNode") {
+ for (let i = 0; i < newNode.widgets.length; i++) {
+ newNode.widgets[i].value = this.node.widgets[widgetIndex + i].value;
+ }
+ } else {
+ const outerWidget = this.node.widgets[widgetIndex];
+ const newWidget = newNode.widgets.find((w) => w.name === oldName);
+ if (!newWidget) continue;
+
+ newWidget.value = outerWidget.value;
+ for (let w = 0; w < outerWidget.linkedWidgets?.length; w++) {
+ newWidget.linkedWidgets[w].value = outerWidget.linkedWidgets[w].value;
+ }
+ }
+ }
+ }
+ }
+
+ // Shift each node
+ for (const newNode of newNodes) {
+ newNode.pos = [newNode.pos[0] - (left - x), newNode.pos[1] - (top - y)];
+ }
+
+ return { newNodes, selectedIds };
+ };
+
+ const reconnectInputs = (selectedIds) => {
+ for (const innerNodeIndex in this.groupData.oldToNewInputMap) {
+ const id = selectedIds[innerNodeIndex];
+ const newNode = app.graph.getNodeById(id);
+ const map = this.groupData.oldToNewInputMap[innerNodeIndex];
+ for (const innerInputId in map) {
+ const groupSlotId = map[innerInputId];
+ if (groupSlotId == null) continue;
+ const slot = node.inputs[groupSlotId];
+ if (slot.link == null) continue;
+ const link = app.graph.links[slot.link];
+ // connect this node output to the input of another node
+ const originNode = app.graph.getNodeById(link.origin_id);
+ originNode.connect(link.origin_slot, newNode, +innerInputId);
+ }
+ }
+ };
+
+ const reconnectOutputs = (selectedIds) => {
+ for (let groupOutputId = 0; groupOutputId < node.outputs?.length; groupOutputId++) {
+ const output = node.outputs[groupOutputId];
+ if (!output.links) continue;
+ const links = [...output.links];
+ for (const l of links) {
+ const slot = this.groupData.newToOldOutputMap[groupOutputId];
+ const link = app.graph.links[l];
+ const targetNode = app.graph.getNodeById(link.target_id);
+ const newNode = app.graph.getNodeById(selectedIds[slot.node.index]);
+ newNode.connect(slot.slot, targetNode, link.target_slot);
+ }
+ }
+ };
+
+ const { newNodes, selectedIds } = addInnerNodes();
+ reconnectInputs(selectedIds);
+ reconnectOutputs(selectedIds);
+ app.graph.remove(this.node);
+
+ return newNodes;
+ };
+
+ const getExtraMenuOptions = this.node.getExtraMenuOptions;
+ this.node.getExtraMenuOptions = function (_, options) {
+ getExtraMenuOptions?.apply(this, arguments);
+
+ let optionIndex = options.findIndex((o) => o.content === "Outputs");
+ if (optionIndex === -1) optionIndex = options.length;
+ else optionIndex++;
+ options.splice(optionIndex, 0, null, {
+ content: "Convert to nodes",
+ callback: () => {
+ return this.convertToNodes();
+ },
+ });
+ };
+
+ // Draw custom collapse icon to identity this as a group
+ const onDrawTitleBox = this.node.onDrawTitleBox;
+ this.node.onDrawTitleBox = function (ctx, height, size, scale) {
+ onDrawTitleBox?.apply(this, arguments);
+
+ const fill = ctx.fillStyle;
+ ctx.beginPath();
+ ctx.rect(11, -height + 11, 2, 2);
+ ctx.rect(14, -height + 11, 2, 2);
+ ctx.rect(17, -height + 11, 2, 2);
+ ctx.rect(11, -height + 14, 2, 2);
+ ctx.rect(14, -height + 14, 2, 2);
+ ctx.rect(17, -height + 14, 2, 2);
+ ctx.rect(11, -height + 17, 2, 2);
+ ctx.rect(14, -height + 17, 2, 2);
+ ctx.rect(17, -height + 17, 2, 2);
+
+ ctx.fillStyle = this.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
+ ctx.fill();
+ ctx.fillStyle = fill;
+ };
+
+ // Draw progress label
+ const onDrawForeground = node.onDrawForeground;
+ const groupData = this.groupData.nodeData;
+ node.onDrawForeground = function (ctx) {
+ const r = onDrawForeground?.apply?.(this, arguments);
+ if (+app.runningNodeId === this.id && this.runningInternalNodeId !== null) {
+ const n = groupData.nodes[this.runningInternalNodeId];
+ const message = `Running ${n.title || n.type} (${this.runningInternalNodeId}/${groupData.nodes.length})`;
+ ctx.save();
+ ctx.font = "12px sans-serif";
+ const sz = ctx.measureText(message);
+ ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
+ ctx.beginPath();
+ ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5);
+ ctx.fill();
+
+ ctx.fillStyle = "#fff";
+ ctx.fillText(message, 6, -LiteGraph.NODE_TITLE_HEIGHT - 6);
+ ctx.restore();
+ }
+ };
+
+ // Flag this node as needing to be reset
+ const onExecutionStart = this.node.onExecutionStart;
+ this.node.onExecutionStart = function () {
+ this.resetExecution = true;
+ return onExecutionStart?.apply(this, arguments);
+ };
+
+ function handleEvent(type, getId, getEvent) {
+ const handler = ({ detail }) => {
+ const id = getId(detail);
+ if (!id) return;
+ const node = app.graph.getNodeById(id);
+ if (node) return;
+
+ const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == id);
+ if (innerNodeIndex > -1) {
+ this.node.runningInternalNodeId = innerNodeIndex;
+ api.dispatchEvent(new CustomEvent(type, { detail: getEvent(detail, this.node.id + "", this.node) }));
+ }
+ };
+ api.addEventListener(type, handler);
+ return handler;
+ }
+
+ const executing = handleEvent.call(
+ this,
+ "executing",
+ (d) => d,
+ (d, id, node) => id
+ );
+
+ const executed = handleEvent.call(
+ this,
+ "executed",
+ (d) => d?.node,
+ (d, id, node) => ({ ...d, node: id, merge: !node.resetExecution })
+ );
+
+ const onRemoved = node.onRemoved;
+ this.node.onRemoved = function () {
+ onRemoved?.apply(this, arguments);
+ api.removeEventListener("executing", executing);
+ api.removeEventListener("executed", executed);
+ };
+ }
+
+ updateInnerWidgets() {
+ for (const newWidgetName in this.groupData.newToOldWidgetMap) {
+ const newWidget = this.node.widgets.find((w) => w.name === newWidgetName);
+ if (!newWidget) continue;
+
+ const newValue = newWidget.value;
+ const old = this.groupData.newToOldWidgetMap[newWidgetName];
+ let innerNode = this.innerNodes[old.node.index];
+
+ if (innerNode.type === "PrimitiveNode") {
+ innerNode.primitiveValue = newValue;
+ const primitiveLinked = this.groupData.primitiveToWidget[old.node.index];
+ for (const linked of primitiveLinked ?? []) {
+ const node = this.innerNodes[linked.nodeId];
+ const widget = node.widgets.find((w) => w.name === linked.inputName);
+
+ if (widget) {
+ widget.value = newValue;
+ }
+ }
+ continue;
+ } else if (innerNode.type === "Reroute") {
+ const rerouteLinks = this.groupData.linksFrom[old.node.index];
+ for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) {
+ const node = this.innerNodes[targetNodeId];
+ const input = node.inputs[targetSlot];
+ if (input.widget) {
+ const widget = node.widgets?.find((w) => w.name === input.widget.name);
+ if (widget) {
+ widget.value = newValue;
+ }
+ }
+ }
+ }
+
+ const widget = innerNode.widgets?.find((w) => w.name === old.inputName);
+ if (widget) {
+ widget.value = newValue;
+ }
+ }
+ }
+
+ populatePrimitive(node, nodeId, oldName, i, linkedShift) {
+ // Converted widget, populate primitive if linked
+ const primitiveId = this.groupData.widgetToPrimitive[nodeId]?.[oldName];
+ if (primitiveId == null) return;
+ const targetWidgetName = this.groupData.oldToNewWidgetMap[primitiveId]["value"];
+ const targetWidgetIndex = this.node.widgets.findIndex((w) => w.name === targetWidgetName);
+ if (targetWidgetIndex > -1) {
+ const primitiveNode = this.innerNodes[primitiveId];
+ let len = primitiveNode.widgets.length;
+ if (len - 1 !== this.node.widgets[targetWidgetIndex].linkedWidgets?.length) {
+ // Fallback handling for if some reason the primitive has a different number of widgets
+ // we dont want to overwrite random widgets, better to leave blank
+ len = 1;
+ }
+ for (let i = 0; i < len; i++) {
+ this.node.widgets[targetWidgetIndex + i].value = primitiveNode.widgets[i].value;
+ }
+ }
+ return true;
+ }
+
+ populateReroute(node, nodeId, map) {
+ if (node.type !== "Reroute") return;
+
+ const link = this.groupData.linksFrom[nodeId]?.[0]?.[0];
+ if (!link) return;
+ const [, , targetNodeId, targetNodeSlot] = link;
+ const targetNode = this.groupData.nodeData.nodes[targetNodeId];
+ const inputs = targetNode.inputs;
+ const targetWidget = inputs?.[targetNodeSlot].widget;
+ if (!targetWidget) return;
+
+ const offset = inputs.length - (targetNode.widgets_values?.length ?? 0);
+ const v = targetNode.widgets_values?.[targetNodeSlot - offset];
+ if (v == null) return;
+
+ const widgetName = Object.values(map)[0];
+ const widget = this.node.widgets.find(w => w.name === widgetName);
+ if(widget) {
+ widget.value = v;
+ }
+ }
+
+
+ populateWidgets() {
+ if (!this.node.widgets) return;
+
+ for (let nodeId = 0; nodeId < this.groupData.nodeData.nodes.length; nodeId++) {
+ const node = this.groupData.nodeData.nodes[nodeId];
+ const map = this.groupData.oldToNewWidgetMap[nodeId] ?? {};
+ const widgets = Object.keys(map);
+
+ if (!node.widgets_values?.length) {
+ // special handling for populating values into reroutes
+ // this allows primitives connect to them to pick up the correct value
+ this.populateReroute(node, nodeId, map);
+ continue;
+ }
+
+ let linkedShift = 0;
+ for (let i = 0; i < widgets.length; i++) {
+ const oldName = widgets[i];
+ const newName = map[oldName];
+ const widgetIndex = this.node.widgets.findIndex((w) => w.name === newName);
+ const mainWidget = this.node.widgets[widgetIndex];
+ if (this.populatePrimitive(node, nodeId, oldName, i, linkedShift) || widgetIndex === -1) {
+ // Find the inner widget and shift by the number of linked widgets as they will have been removed too
+ const innerWidget = this.innerNodes[nodeId].widgets?.find((w) => w.name === oldName);
+ linkedShift += innerWidget?.linkedWidgets?.length ?? 0;
+ }
+ if (widgetIndex === -1) {
+ continue;
+ }
+
+ // Populate the main and any linked widget
+ mainWidget.value = node.widgets_values[i + linkedShift];
+ for (let w = 0; w < mainWidget.linkedWidgets?.length; w++) {
+ this.node.widgets[widgetIndex + w + 1].value = node.widgets_values[i + ++linkedShift];
+ }
+ }
+ }
+ }
+
+ replaceNodes(nodes) {
+ let top;
+ let left;
+
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+ if (left == null || node.pos[0] < left) {
+ left = node.pos[0];
+ }
+ if (top == null || node.pos[1] < top) {
+ top = node.pos[1];
+ }
+
+ this.linkOutputs(node, i);
+ app.graph.remove(node);
+ }
+
+ this.linkInputs();
+ this.node.pos = [left, top];
+ }
+
+ linkOutputs(originalNode, nodeId) {
+ if (!originalNode.outputs) return;
+
+ for (const output of originalNode.outputs) {
+ if (!output.links) continue;
+ // Clone the links as they'll be changed if we reconnect
+ const links = [...output.links];
+ for (const l of links) {
+ const link = app.graph.links[l];
+ if (!link) continue;
+
+ const targetNode = app.graph.getNodeById(link.target_id);
+ const newSlot = this.groupData.oldToNewOutputMap[nodeId]?.[link.origin_slot];
+ if (newSlot != null) {
+ this.node.connect(newSlot, targetNode, link.target_slot);
+ }
+ }
+ }
+ }
+
+ linkInputs() {
+ for (const link of this.groupData.nodeData.links ?? []) {
+ const [, originSlot, targetId, targetSlot, actualOriginId] = link;
+ const originNode = app.graph.getNodeById(actualOriginId);
+ if (!originNode) continue; // this node is in the group
+ originNode.connect(originSlot, this.node.id, this.groupData.oldToNewInputMap[targetId][targetSlot]);
+ }
+ }
+
+ static getGroupData(node) {
+ return node.constructor?.nodeData?.[GROUP];
+ }
+
+ static isGroupNode(node) {
+ return !!node.constructor?.nodeData?.[GROUP];
+ }
+
+ static async fromNodes(nodes) {
+ // Process the nodes into the stored workflow group node data
+ const builder = new GroupNodeBuilder(nodes);
+ const res = builder.build();
+ if (!res) return;
+
+ const { name, nodeData } = res;
+
+ // Convert this data into a LG node definition and register it
+ const config = new GroupNodeConfig(name, nodeData);
+ await config.registerType();
+
+ const groupNode = LiteGraph.createNode(`workflow/${name}`);
+ // Reuse the existing nodes for this instance
+ groupNode.setInnerNodes(builder.nodes);
+ groupNode[GROUP].populateWidgets();
+ app.graph.add(groupNode);
+
+ // Remove all converted nodes and relink them
+ groupNode[GROUP].replaceNodes(builder.nodes);
+ return groupNode;
+ }
+}
+
+function addConvertToGroupOptions() {
+ function addOption(options, index) {
+ const selected = Object.values(app.canvas.selected_nodes ?? {});
+ const disabled = selected.length < 2 || selected.find((n) => GroupNodeHandler.isGroupNode(n));
+ options.splice(index + 1, null, {
+ content: `Convert to Group Node`,
+ disabled,
+ callback: async () => {
+ return await GroupNodeHandler.fromNodes(selected);
+ },
+ });
+ }
+
+ // Add to canvas
+ const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
+ LGraphCanvas.prototype.getCanvasMenuOptions = function () {
+ const options = getCanvasMenuOptions.apply(this, arguments);
+ const index = options.findIndex((o) => o?.content === "Add Group") + 1 || options.length;
+ addOption(options, index);
+ return options;
+ };
+
+ // Add to nodes
+ const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
+ LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
+ const options = getNodeMenuOptions.apply(this, arguments);
+ if (!GroupNodeHandler.isGroupNode(node)) {
+ const index = options.findIndex((o) => o?.content === "Outputs") + 1 || options.length - 1;
+ addOption(options, index);
+ }
+ return options;
+ };
+}
+
+const id = "Comfy.GroupNode";
+let globalDefs;
+const ext = {
+ name: id,
+ setup() {
+ addConvertToGroupOptions();
+ },
+ async beforeConfigureGraph(graphData, missingNodeTypes) {
+ const nodes = graphData?.extra?.groupNodes;
+ if (nodes) {
+ await GroupNodeConfig.registerFromWorkflow(nodes, missingNodeTypes);
+ }
+ },
+ addCustomNodeDefs(defs) {
+ // Store this so we can mutate it later with group nodes
+ globalDefs = defs;
+ },
+ nodeCreated(node) {
+ if (GroupNodeHandler.isGroupNode(node)) {
+ node[GROUP] = new GroupNodeHandler(node);
+ }
+ },
+};
+
+app.registerExtension(ext);
diff --git a/ComfyUI/web/extensions/core/groupOptions.js b/ComfyUI/web/extensions/core/groupOptions.js
new file mode 100644
index 0000000000000000000000000000000000000000..5dd21e7301660cfbe34a7804df6f1a94f8b04666
--- /dev/null
+++ b/ComfyUI/web/extensions/core/groupOptions.js
@@ -0,0 +1,259 @@
+import {app} from "../../scripts/app.js";
+
+function setNodeMode(node, mode) {
+ node.mode = mode;
+ node.graph.change();
+}
+
+function addNodesToGroup(group, nodes=[]) {
+ var x1, y1, x2, y2;
+ var nx1, ny1, nx2, ny2;
+ var node;
+
+ x1 = y1 = x2 = y2 = -1;
+ nx1 = ny1 = nx2 = ny2 = -1;
+
+ for (var n of [group._nodes, nodes]) {
+ for (var i in n) {
+ node = n[i]
+
+ nx1 = node.pos[0]
+ ny1 = node.pos[1]
+ nx2 = node.pos[0] + node.size[0]
+ ny2 = node.pos[1] + node.size[1]
+
+ if (node.type != "Reroute") {
+ ny1 -= LiteGraph.NODE_TITLE_HEIGHT;
+ }
+
+ if (node.flags?.collapsed) {
+ ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT;
+
+ if (node?._collapsed_width) {
+ nx2 = nx1 + Math.round(node._collapsed_width);
+ }
+ }
+
+ if (x1 == -1 || nx1 < x1) {
+ x1 = nx1;
+ }
+
+ if (y1 == -1 || ny1 < y1) {
+ y1 = ny1;
+ }
+
+ if (x2 == -1 || nx2 > x2) {
+ x2 = nx2;
+ }
+
+ if (y2 == -1 || ny2 > y2) {
+ y2 = ny2;
+ }
+ }
+ }
+
+ var padding = 10;
+
+ y1 = y1 - Math.round(group.font_size * 1.4);
+
+ group.pos = [x1 - padding, y1 - padding];
+ group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2];
+}
+
+app.registerExtension({
+ name: "Comfy.GroupOptions",
+ setup() {
+ const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
+ // graph_mouse
+ LGraphCanvas.prototype.getCanvasMenuOptions = function () {
+ const options = orig.apply(this, arguments);
+ const group = this.graph.getGroupOnPos(this.graph_mouse[0], this.graph_mouse[1]);
+ if (!group) {
+ options.push({
+ content: "Add Group For Selected Nodes",
+ disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
+ callback: () => {
+ var group = new LiteGraph.LGraphGroup();
+ addNodesToGroup(group, this.selected_nodes)
+ app.canvas.graph.add(group);
+ this.graph.change();
+ }
+ });
+
+ return options;
+ }
+
+ // Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
+ group.recomputeInsideNodes();
+ const nodesInGroup = group._nodes;
+
+ options.push({
+ content: "Add Selected Nodes To Group",
+ disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
+ callback: () => {
+ addNodesToGroup(group, this.selected_nodes)
+ this.graph.change();
+ }
+ });
+
+ // No nodes in group, return default options
+ if (nodesInGroup.length === 0) {
+ return options;
+ } else {
+ // Add a separator between the default options and the group options
+ options.push(null);
+ }
+
+ // Check if all nodes are the same mode
+ let allNodesAreSameMode = true;
+ for (let i = 1; i < nodesInGroup.length; i++) {
+ if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
+ allNodesAreSameMode = false;
+ break;
+ }
+ }
+
+ options.push({
+ content: "Fit Group To Nodes",
+ callback: () => {
+ addNodesToGroup(group)
+ this.graph.change();
+ }
+ });
+
+ options.push({
+ content: "Select Nodes",
+ callback: () => {
+ this.selectNodes(nodesInGroup);
+ this.graph.change();
+ this.canvas.focus();
+ }
+ });
+
+ // Modes
+ // 0: Always
+ // 1: On Event
+ // 2: Never
+ // 3: On Trigger
+ // 4: Bypass
+ // If all nodes are the same mode, add a menu option to change the mode
+ if (allNodesAreSameMode) {
+ const mode = nodesInGroup[0].mode;
+ switch (mode) {
+ case 0:
+ // All nodes are always, option to disable, and bypass
+ options.push({
+ content: "Set Group Nodes to Never",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 2);
+ }
+ }
+ });
+ options.push({
+ content: "Bypass Group Nodes",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 4);
+ }
+ }
+ });
+ break;
+ case 2:
+ // All nodes are never, option to enable, and bypass
+ options.push({
+ content: "Set Group Nodes to Always",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 0);
+ }
+ }
+ });
+ options.push({
+ content: "Bypass Group Nodes",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 4);
+ }
+ }
+ });
+ break;
+ case 4:
+ // All nodes are bypass, option to enable, and disable
+ options.push({
+ content: "Set Group Nodes to Always",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 0);
+ }
+ }
+ });
+ options.push({
+ content: "Set Group Nodes to Never",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 2);
+ }
+ }
+ });
+ break;
+ default:
+ // All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass
+ options.push({
+ content: "Set Group Nodes to Always",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 0);
+ }
+ }
+ });
+ options.push({
+ content: "Set Group Nodes to Never",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 2);
+ }
+ }
+ });
+ options.push({
+ content: "Bypass Group Nodes",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 4);
+ }
+ }
+ });
+ break;
+ }
+ } else {
+ // Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass
+ options.push({
+ content: "Set Group Nodes to Always",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 0);
+ }
+ }
+ });
+ options.push({
+ content: "Set Group Nodes to Never",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 2);
+ }
+ }
+ });
+ options.push({
+ content: "Bypass Group Nodes",
+ callback: () => {
+ for (const node of nodesInGroup) {
+ setNodeMode(node, 4);
+ }
+ }
+ });
+ }
+
+ return options
+ }
+ }
+});
diff --git a/ComfyUI/web/extensions/core/invertMenuScrolling.js b/ComfyUI/web/extensions/core/invertMenuScrolling.js
new file mode 100644
index 0000000000000000000000000000000000000000..98a1786ab48972ad3a92f4f7cda8fa4273e0bde6
--- /dev/null
+++ b/ComfyUI/web/extensions/core/invertMenuScrolling.js
@@ -0,0 +1,36 @@
+import { app } from "../../scripts/app.js";
+
+// Inverts the scrolling of context menus
+
+const id = "Comfy.InvertMenuScrolling";
+app.registerExtension({
+ name: id,
+ init() {
+ const ctxMenu = LiteGraph.ContextMenu;
+ const replace = () => {
+ LiteGraph.ContextMenu = function (values, options) {
+ options = options || {};
+ if (options.scroll_speed) {
+ options.scroll_speed *= -1;
+ } else {
+ options.scroll_speed = -0.1;
+ }
+ return ctxMenu.call(this, values, options);
+ };
+ LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
+ };
+ app.ui.settings.addSetting({
+ id,
+ name: "Invert Menu Scrolling",
+ type: "boolean",
+ defaultValue: false,
+ onChange(value) {
+ if (value) {
+ replace();
+ } else {
+ LiteGraph.ContextMenu = ctxMenu;
+ }
+ },
+ });
+ },
+});
diff --git a/ComfyUI/web/extensions/core/keybinds.js b/ComfyUI/web/extensions/core/keybinds.js
new file mode 100644
index 0000000000000000000000000000000000000000..cf698ea5a66cebfb3c8e9192d4d192503b461697
--- /dev/null
+++ b/ComfyUI/web/extensions/core/keybinds.js
@@ -0,0 +1,70 @@
+import {app} from "../../scripts/app.js";
+
+app.registerExtension({
+ name: "Comfy.Keybinds",
+ init() {
+ const keybindListener = function (event) {
+ const modifierPressed = event.ctrlKey || event.metaKey;
+
+ // Queue prompt using ctrl or command + enter
+ if (modifierPressed && event.key === "Enter") {
+ app.queuePrompt(event.shiftKey ? -1 : 0).then();
+ return;
+ }
+
+ const target = event.composedPath()[0];
+ if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
+ return;
+ }
+
+ const modifierKeyIdMap = {
+ s: "#comfy-save-button",
+ o: "#comfy-file-input",
+ Backspace: "#comfy-clear-button",
+ Delete: "#comfy-clear-button",
+ d: "#comfy-load-default-button",
+ };
+
+ const modifierKeybindId = modifierKeyIdMap[event.key];
+ if (modifierPressed && modifierKeybindId) {
+ event.preventDefault();
+
+ const elem = document.querySelector(modifierKeybindId);
+ elem.click();
+ return;
+ }
+
+ // Finished Handling all modifier keybinds, now handle the rest
+ if (event.ctrlKey || event.altKey || event.metaKey) {
+ return;
+ }
+
+ // Close out of modals using escape
+ if (event.key === "Escape") {
+ const modals = document.querySelectorAll(".comfy-modal");
+ const modal = Array.from(modals).find(modal => window.getComputedStyle(modal).getPropertyValue("display") !== "none");
+ if (modal) {
+ modal.style.display = "none";
+ }
+
+ [...document.querySelectorAll("dialog")].forEach(d => {
+ d.close();
+ });
+ }
+
+ const keyIdMap = {
+ q: "#comfy-view-queue-button",
+ h: "#comfy-view-history-button",
+ r: "#comfy-refresh-button",
+ };
+
+ const buttonId = keyIdMap[event.key];
+ if (buttonId) {
+ const button = document.querySelector(buttonId);
+ button.click();
+ }
+ }
+
+ window.addEventListener("keydown", keybindListener, true);
+ }
+});
diff --git a/ComfyUI/web/extensions/core/linkRenderMode.js b/ComfyUI/web/extensions/core/linkRenderMode.js
new file mode 100644
index 0000000000000000000000000000000000000000..fb4df4234e587817c150be71059113f10443b01a
--- /dev/null
+++ b/ComfyUI/web/extensions/core/linkRenderMode.js
@@ -0,0 +1,25 @@
+import { app } from "../../scripts/app.js";
+
+const id = "Comfy.LinkRenderMode";
+const ext = {
+ name: id,
+ async setup(app) {
+ app.ui.settings.addSetting({
+ id,
+ name: "Link Render Mode",
+ defaultValue: 2,
+ type: "combo",
+ options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({
+ value: i,
+ text: m,
+ selected: i == app.canvas.links_render_mode,
+ })),
+ onChange(value) {
+ app.canvas.links_render_mode = +value;
+ app.graph.setDirtyCanvas(true);
+ },
+ });
+ },
+};
+
+app.registerExtension(ext);
diff --git a/ComfyUI/web/extensions/core/maskeditor.js b/ComfyUI/web/extensions/core/maskeditor.js
new file mode 100644
index 0000000000000000000000000000000000000000..bb2f16d42b511034a4eb9431da642fdfa7bb3b6d
--- /dev/null
+++ b/ComfyUI/web/extensions/core/maskeditor.js
@@ -0,0 +1,800 @@
+import { app } from "../../scripts/app.js";
+import { ComfyDialog, $el } from "../../scripts/ui.js";
+import { ComfyApp } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js"
+import { ClipspaceDialog } from "./clipspace.js";
+
+// Helper function to convert a data URL to a Blob object
+function dataURLToBlob(dataURL) {
+ const parts = dataURL.split(';base64,');
+ const contentType = parts[0].split(':')[1];
+ const byteString = atob(parts[1]);
+ const arrayBuffer = new ArrayBuffer(byteString.length);
+ const uint8Array = new Uint8Array(arrayBuffer);
+ for (let i = 0; i < byteString.length; i++) {
+ uint8Array[i] = byteString.charCodeAt(i);
+ }
+ return new Blob([arrayBuffer], { type: contentType });
+}
+
+function loadedImageToBlob(image) {
+ const canvas = document.createElement('canvas');
+
+ canvas.width = image.width;
+ canvas.height = image.height;
+
+ const ctx = canvas.getContext('2d');
+
+ ctx.drawImage(image, 0, 0);
+
+ const dataURL = canvas.toDataURL('image/png', 1);
+ const blob = dataURLToBlob(dataURL);
+
+ return blob;
+}
+
+function loadImage(imagePath) {
+ return new Promise((resolve, reject) => {
+ const image = new Image();
+
+ image.onload = function() {
+ resolve(image);
+ };
+
+ image.src = imagePath;
+ });
+}
+
+async function uploadMask(filepath, formData) {
+ await api.fetchApi('/upload/mask', {
+ method: 'POST',
+ body: formData
+ }).then(response => {}).catch(error => {
+ console.error('Error:', error);
+ });
+
+ ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image();
+ ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = api.apiURL("/view?" + new URLSearchParams(filepath).toString() + app.getPreviewFormatParam() + app.getRandParam());
+
+ if(ComfyApp.clipspace.images)
+ ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath;
+
+ ClipspaceDialog.invalidatePreview();
+}
+
+function prepare_mask(image, maskCanvas, maskCtx) {
+ // paste mask data into alpha channel
+ maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height);
+ const maskData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height);
+
+ // invert mask
+ for (let i = 0; i < maskData.data.length; i += 4) {
+ if(maskData.data[i+3] == 255)
+ maskData.data[i+3] = 0;
+ else
+ maskData.data[i+3] = 255;
+
+ maskData.data[i] = 0;
+ maskData.data[i+1] = 0;
+ maskData.data[i+2] = 0;
+ }
+
+ maskCtx.globalCompositeOperation = 'source-over';
+ maskCtx.putImageData(maskData, 0, 0);
+}
+
+class MaskEditorDialog extends ComfyDialog {
+ static instance = null;
+
+ static getInstance() {
+ if(!MaskEditorDialog.instance) {
+ MaskEditorDialog.instance = new MaskEditorDialog(app);
+ }
+
+ return MaskEditorDialog.instance;
+ }
+
+ is_layout_created = false;
+
+ constructor() {
+ super();
+ this.element = $el("div.comfy-modal", { parent: document.body },
+ [ $el("div.comfy-modal-content",
+ [...this.createButtons()]),
+ ]);
+ }
+
+ createButtons() {
+ return [];
+ }
+
+ createButton(name, callback) {
+ var button = document.createElement("button");
+ button.innerText = name;
+ button.addEventListener("click", callback);
+ return button;
+ }
+
+ createLeftButton(name, callback) {
+ var button = this.createButton(name, callback);
+ button.style.cssFloat = "left";
+ button.style.marginRight = "4px";
+ return button;
+ }
+
+ createRightButton(name, callback) {
+ var button = this.createButton(name, callback);
+ button.style.cssFloat = "right";
+ button.style.marginLeft = "4px";
+ return button;
+ }
+
+ createLeftSlider(self, name, callback) {
+ const divElement = document.createElement('div');
+ divElement.id = "maskeditor-slider";
+ divElement.style.cssFloat = "left";
+ divElement.style.fontFamily = "sans-serif";
+ divElement.style.marginRight = "4px";
+ divElement.style.color = "var(--input-text)";
+ divElement.style.backgroundColor = "var(--comfy-input-bg)";
+ divElement.style.borderRadius = "8px";
+ divElement.style.borderColor = "var(--border-color)";
+ divElement.style.borderStyle = "solid";
+ divElement.style.fontSize = "15px";
+ divElement.style.height = "21px";
+ divElement.style.padding = "1px 6px";
+ divElement.style.display = "flex";
+ divElement.style.position = "relative";
+ divElement.style.top = "2px";
+ self.brush_slider_input = document.createElement('input');
+ self.brush_slider_input.setAttribute('type', 'range');
+ self.brush_slider_input.setAttribute('min', '1');
+ self.brush_slider_input.setAttribute('max', '100');
+ self.brush_slider_input.setAttribute('value', '10');
+ const labelElement = document.createElement("label");
+ labelElement.textContent = name;
+
+ divElement.appendChild(labelElement);
+ divElement.appendChild(self.brush_slider_input);
+
+ self.brush_slider_input.addEventListener("change", callback);
+
+ return divElement;
+ }
+
+ setlayout(imgCanvas, maskCanvas) {
+ const self = this;
+
+ // If it is specified as relative, using it only as a hidden placeholder for padding is recommended
+ // to prevent anomalies where it exceeds a certain size and goes outside of the window.
+ var bottom_panel = document.createElement("div");
+ bottom_panel.style.position = "absolute";
+ bottom_panel.style.bottom = "0px";
+ bottom_panel.style.left = "20px";
+ bottom_panel.style.right = "20px";
+ bottom_panel.style.height = "50px";
+
+ var brush = document.createElement("div");
+ brush.id = "brush";
+ brush.style.backgroundColor = "transparent";
+ brush.style.outline = "1px dashed black";
+ brush.style.boxShadow = "0 0 0 1px white";
+ brush.style.borderRadius = "50%";
+ brush.style.MozBorderRadius = "50%";
+ brush.style.WebkitBorderRadius = "50%";
+ brush.style.position = "absolute";
+ brush.style.zIndex = 8889;
+ brush.style.pointerEvents = "none";
+ this.brush = brush;
+ this.element.appendChild(imgCanvas);
+ this.element.appendChild(maskCanvas);
+ this.element.appendChild(bottom_panel);
+ document.body.appendChild(brush);
+
+ this.brush_size_slider = this.createLeftSlider(self, "Thickness", (event) => {
+ self.brush_size = event.target.value;
+ self.updateBrushPreview(self, null, null);
+ });
+ var clearButton = this.createLeftButton("Clear",
+ () => {
+ self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
+ });
+ var cancelButton = this.createRightButton("Cancel", () => {
+ document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
+ document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
+ self.close();
+ });
+
+ this.saveButton = this.createRightButton("Save", () => {
+ document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
+ document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
+ self.save();
+ });
+
+ this.element.appendChild(imgCanvas);
+ this.element.appendChild(maskCanvas);
+ this.element.appendChild(bottom_panel);
+
+ bottom_panel.appendChild(clearButton);
+ bottom_panel.appendChild(this.saveButton);
+ bottom_panel.appendChild(cancelButton);
+ bottom_panel.appendChild(this.brush_size_slider);
+
+ imgCanvas.style.position = "absolute";
+ maskCanvas.style.position = "absolute";
+
+ imgCanvas.style.top = "200";
+ imgCanvas.style.left = "0";
+
+ maskCanvas.style.top = imgCanvas.style.top;
+ maskCanvas.style.left = imgCanvas.style.left;
+ }
+
+ async show() {
+ this.zoom_ratio = 1.0;
+ this.pan_x = 0;
+ this.pan_y = 0;
+
+ if(!this.is_layout_created) {
+ // layout
+ const imgCanvas = document.createElement('canvas');
+ const maskCanvas = document.createElement('canvas');
+
+ imgCanvas.id = "imageCanvas";
+ maskCanvas.id = "maskCanvas";
+
+ this.setlayout(imgCanvas, maskCanvas);
+
+ // prepare content
+ this.imgCanvas = imgCanvas;
+ this.maskCanvas = maskCanvas;
+ this.maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true });
+
+ this.setEventHandler(maskCanvas);
+
+ this.is_layout_created = true;
+
+ // replacement of onClose hook since close is not real close
+ const self = this;
+ const observer = new MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
+ if(self.last_display_style && self.last_display_style != 'none' && self.element.style.display == 'none') {
+ document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
+ self.brush.style.display = "none";
+ ComfyApp.onClipspaceEditorClosed();
+ }
+
+ self.last_display_style = self.element.style.display;
+ }
+ });
+ });
+
+ const config = { attributes: true };
+ observer.observe(this.element, config);
+ }
+
+ // The keydown event needs to be reconfigured when closing the dialog as it gets removed.
+ document.addEventListener('keydown', MaskEditorDialog.handleKeyDown);
+
+ if(ComfyApp.clipspace_return_node) {
+ this.saveButton.innerText = "Save to node";
+ }
+ else {
+ this.saveButton.innerText = "Save";
+ }
+ this.saveButton.disabled = false;
+
+ this.element.style.display = "block";
+ this.element.style.width = "85%";
+ this.element.style.margin = "0 7.5%";
+ this.element.style.height = "100vh";
+ this.element.style.top = "50%";
+ this.element.style.left = "42%";
+ this.element.style.zIndex = 8888; // NOTE: alert dialog must be high priority.
+
+ await this.setImages(this.imgCanvas);
+
+ this.is_visible = true;
+ }
+
+ isOpened() {
+ return this.element.style.display == "block";
+ }
+
+ invalidateCanvas(orig_image, mask_image) {
+ this.imgCanvas.width = orig_image.width;
+ this.imgCanvas.height = orig_image.height;
+
+ this.maskCanvas.width = orig_image.width;
+ this.maskCanvas.height = orig_image.height;
+
+ let imgCtx = this.imgCanvas.getContext('2d', {willReadFrequently: true });
+ let maskCtx = this.maskCanvas.getContext('2d', {willReadFrequently: true });
+
+ imgCtx.drawImage(orig_image, 0, 0, orig_image.width, orig_image.height);
+ prepare_mask(mask_image, this.maskCanvas, maskCtx);
+ }
+
+ async setImages(imgCanvas) {
+ let self = this;
+
+ const imgCtx = imgCanvas.getContext('2d', {willReadFrequently: true });
+ const maskCtx = this.maskCtx;
+ const maskCanvas = this.maskCanvas;
+
+ imgCtx.clearRect(0,0,this.imgCanvas.width,this.imgCanvas.height);
+ maskCtx.clearRect(0,0,this.maskCanvas.width,this.maskCanvas.height);
+
+ // image load
+ const filepath = ComfyApp.clipspace.images;
+
+ const alpha_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src)
+ alpha_url.searchParams.delete('channel');
+ alpha_url.searchParams.delete('preview');
+ alpha_url.searchParams.set('channel', 'a');
+ let mask_image = await loadImage(alpha_url);
+
+ // original image load
+ const rgb_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src);
+ rgb_url.searchParams.delete('channel');
+ rgb_url.searchParams.set('channel', 'rgb');
+ this.image = new Image();
+ this.image.onload = function() {
+ maskCanvas.width = self.image.width;
+ maskCanvas.height = self.image.height;
+
+ self.invalidateCanvas(self.image, mask_image);
+ self.initializeCanvasPanZoom();
+ };
+ this.image.src = rgb_url;
+ }
+
+ initializeCanvasPanZoom() {
+ // set initialize
+ let drawWidth = this.image.width;
+ let drawHeight = this.image.height;
+
+ let width = this.element.clientWidth;
+ let height = this.element.clientHeight;
+
+ if (this.image.width > width) {
+ drawWidth = width;
+ drawHeight = (drawWidth / this.image.width) * this.image.height;
+ }
+
+ if (drawHeight > height) {
+ drawHeight = height;
+ drawWidth = (drawHeight / this.image.height) * this.image.width;
+ }
+
+ this.zoom_ratio = drawWidth/this.image.width;
+
+ const canvasX = (width - drawWidth) / 2;
+ const canvasY = (height - drawHeight) / 2;
+ this.pan_x = canvasX;
+ this.pan_y = canvasY;
+
+ this.invalidatePanZoom();
+ }
+
+
+ invalidatePanZoom() {
+ let raw_width = this.image.width * this.zoom_ratio;
+ let raw_height = this.image.height * this.zoom_ratio;
+
+ if(this.pan_x + raw_width < 10) {
+ this.pan_x = 10 - raw_width;
+ }
+
+ if(this.pan_y + raw_height < 10) {
+ this.pan_y = 10 - raw_height;
+ }
+
+ let width = `${raw_width}px`;
+ let height = `${raw_height}px`;
+
+ let left = `${this.pan_x}px`;
+ let top = `${this.pan_y}px`;
+
+ this.maskCanvas.style.width = width;
+ this.maskCanvas.style.height = height;
+ this.maskCanvas.style.left = left;
+ this.maskCanvas.style.top = top;
+
+ this.imgCanvas.style.width = width;
+ this.imgCanvas.style.height = height;
+ this.imgCanvas.style.left = left;
+ this.imgCanvas.style.top = top;
+ }
+
+
+ setEventHandler(maskCanvas) {
+ const self = this;
+
+ if(!this.handler_registered) {
+ maskCanvas.addEventListener("contextmenu", (event) => {
+ event.preventDefault();
+ });
+
+ this.element.addEventListener('wheel', (event) => this.handleWheelEvent(self,event));
+ this.element.addEventListener('pointermove', (event) => this.pointMoveEvent(self,event));
+ this.element.addEventListener('touchmove', (event) => this.pointMoveEvent(self,event));
+
+ this.element.addEventListener('dragstart', (event) => {
+ if(event.ctrlKey) {
+ event.preventDefault();
+ }
+ });
+
+ maskCanvas.addEventListener('pointerdown', (event) => this.handlePointerDown(self,event));
+ maskCanvas.addEventListener('pointermove', (event) => this.draw_move(self,event));
+ maskCanvas.addEventListener('touchmove', (event) => this.draw_move(self,event));
+ maskCanvas.addEventListener('pointerover', (event) => { this.brush.style.display = "block"; });
+ maskCanvas.addEventListener('pointerleave', (event) => { this.brush.style.display = "none"; });
+
+ document.addEventListener('pointerup', MaskEditorDialog.handlePointerUp);
+
+ this.handler_registered = true;
+ }
+ }
+
+ brush_size = 10;
+ drawing_mode = false;
+ lastx = -1;
+ lasty = -1;
+ lasttime = 0;
+
+ static handleKeyDown(event) {
+ const self = MaskEditorDialog.instance;
+ if (event.key === ']') {
+ self.brush_size = Math.min(self.brush_size+2, 100);
+ self.brush_slider_input.value = self.brush_size;
+ } else if (event.key === '[') {
+ self.brush_size = Math.max(self.brush_size-2, 1);
+ self.brush_slider_input.value = self.brush_size;
+ } else if(event.key === 'Enter') {
+ self.save();
+ }
+
+ self.updateBrushPreview(self);
+ }
+
+ static handlePointerUp(event) {
+ event.preventDefault();
+
+ this.mousedown_x = null;
+ this.mousedown_y = null;
+
+ MaskEditorDialog.instance.drawing_mode = false;
+ }
+
+ updateBrushPreview(self) {
+ const brush = self.brush;
+
+ var centerX = self.cursorX;
+ var centerY = self.cursorY;
+
+ brush.style.width = self.brush_size * 2 * this.zoom_ratio + "px";
+ brush.style.height = self.brush_size * 2 * this.zoom_ratio + "px";
+ brush.style.left = (centerX - self.brush_size * this.zoom_ratio) + "px";
+ brush.style.top = (centerY - self.brush_size * this.zoom_ratio) + "px";
+ }
+
+ handleWheelEvent(self, event) {
+ event.preventDefault();
+
+ if(event.ctrlKey) {
+ // zoom canvas
+ if(event.deltaY < 0) {
+ this.zoom_ratio = Math.min(10.0, this.zoom_ratio+0.2);
+ }
+ else {
+ this.zoom_ratio = Math.max(0.2, this.zoom_ratio-0.2);
+ }
+
+ this.invalidatePanZoom();
+ }
+ else {
+ // adjust brush size
+ if(event.deltaY < 0)
+ this.brush_size = Math.min(this.brush_size+2, 100);
+ else
+ this.brush_size = Math.max(this.brush_size-2, 1);
+
+ this.brush_slider_input.value = this.brush_size;
+
+ this.updateBrushPreview(this);
+ }
+ }
+
+ pointMoveEvent(self, event) {
+ this.cursorX = event.pageX;
+ this.cursorY = event.pageY;
+
+ self.updateBrushPreview(self);
+
+ if(event.ctrlKey) {
+ event.preventDefault();
+ self.pan_move(self, event);
+ }
+ }
+
+ pan_move(self, event) {
+ if(event.buttons == 1) {
+ if(this.mousedown_x) {
+ let deltaX = this.mousedown_x - event.clientX;
+ let deltaY = this.mousedown_y - event.clientY;
+
+ self.pan_x = this.mousedown_pan_x - deltaX;
+ self.pan_y = this.mousedown_pan_y - deltaY;
+
+ self.invalidatePanZoom();
+ }
+ }
+ }
+
+ draw_move(self, event) {
+ if(event.ctrlKey) {
+ return;
+ }
+
+ event.preventDefault();
+
+ this.cursorX = event.pageX;
+ this.cursorY = event.pageY;
+
+ self.updateBrushPreview(self);
+
+ if (window.TouchEvent && event instanceof TouchEvent || event.buttons == 1) {
+ var diff = performance.now() - self.lasttime;
+
+ const maskRect = self.maskCanvas.getBoundingClientRect();
+
+ var x = event.offsetX;
+ var y = event.offsetY
+
+ if(event.offsetX == null) {
+ x = event.targetTouches[0].clientX - maskRect.left;
+ }
+
+ if(event.offsetY == null) {
+ y = event.targetTouches[0].clientY - maskRect.top;
+ }
+
+ x /= self.zoom_ratio;
+ y /= self.zoom_ratio;
+
+ var brush_size = this.brush_size;
+ if(event instanceof PointerEvent && event.pointerType == 'pen') {
+ brush_size *= event.pressure;
+ this.last_pressure = event.pressure;
+ }
+ else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){
+ // The firing interval of PointerEvents in Pen is unreliable, so it is supplemented by TouchEvents.
+ brush_size *= this.last_pressure;
+ }
+ else {
+ brush_size = this.brush_size;
+ }
+
+ if(diff > 20 && !this.drawing_mode)
+ requestAnimationFrame(() => {
+ self.maskCtx.beginPath();
+ self.maskCtx.fillStyle = "rgb(0,0,0)";
+ self.maskCtx.globalCompositeOperation = "source-over";
+ self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
+ self.maskCtx.fill();
+ self.lastx = x;
+ self.lasty = y;
+ });
+ else
+ requestAnimationFrame(() => {
+ self.maskCtx.beginPath();
+ self.maskCtx.fillStyle = "rgb(0,0,0)";
+ self.maskCtx.globalCompositeOperation = "source-over";
+
+ var dx = x - self.lastx;
+ var dy = y - self.lasty;
+
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ var directionX = dx / distance;
+ var directionY = dy / distance;
+
+ for (var i = 0; i < distance; i+=5) {
+ var px = self.lastx + (directionX * i);
+ var py = self.lasty + (directionY * i);
+ self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
+ self.maskCtx.fill();
+ }
+ self.lastx = x;
+ self.lasty = y;
+ });
+
+ self.lasttime = performance.now();
+ }
+ else if(event.buttons == 2 || event.buttons == 5 || event.buttons == 32) {
+ const maskRect = self.maskCanvas.getBoundingClientRect();
+ const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
+ const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
+
+ var brush_size = this.brush_size;
+ if(event instanceof PointerEvent && event.pointerType == 'pen') {
+ brush_size *= event.pressure;
+ this.last_pressure = event.pressure;
+ }
+ else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){
+ brush_size *= this.last_pressure;
+ }
+ else {
+ brush_size = this.brush_size;
+ }
+
+ if(diff > 20 && !drawing_mode) // cannot tracking drawing_mode for touch event
+ requestAnimationFrame(() => {
+ self.maskCtx.beginPath();
+ self.maskCtx.globalCompositeOperation = "destination-out";
+ self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
+ self.maskCtx.fill();
+ self.lastx = x;
+ self.lasty = y;
+ });
+ else
+ requestAnimationFrame(() => {
+ self.maskCtx.beginPath();
+ self.maskCtx.globalCompositeOperation = "destination-out";
+
+ var dx = x - self.lastx;
+ var dy = y - self.lasty;
+
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ var directionX = dx / distance;
+ var directionY = dy / distance;
+
+ for (var i = 0; i < distance; i+=5) {
+ var px = self.lastx + (directionX * i);
+ var py = self.lasty + (directionY * i);
+ self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
+ self.maskCtx.fill();
+ }
+ self.lastx = x;
+ self.lasty = y;
+ });
+
+ self.lasttime = performance.now();
+ }
+ }
+
+ handlePointerDown(self, event) {
+ if(event.ctrlKey) {
+ if (event.buttons == 1) {
+ this.mousedown_x = event.clientX;
+ this.mousedown_y = event.clientY;
+
+ this.mousedown_pan_x = this.pan_x;
+ this.mousedown_pan_y = this.pan_y;
+ }
+ return;
+ }
+
+ var brush_size = this.brush_size;
+ if(event instanceof PointerEvent && event.pointerType == 'pen') {
+ brush_size *= event.pressure;
+ this.last_pressure = event.pressure;
+ }
+
+ if ([0, 2, 5].includes(event.button)) {
+ self.drawing_mode = true;
+
+ event.preventDefault();
+ const maskRect = self.maskCanvas.getBoundingClientRect();
+ const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
+ const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
+
+ self.maskCtx.beginPath();
+ if (event.button == 0) {
+ self.maskCtx.fillStyle = "rgb(0,0,0)";
+ self.maskCtx.globalCompositeOperation = "source-over";
+ } else {
+ self.maskCtx.globalCompositeOperation = "destination-out";
+ }
+ self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
+ self.maskCtx.fill();
+ self.lastx = x;
+ self.lasty = y;
+ self.lasttime = performance.now();
+ }
+ }
+
+ async save() {
+ const backupCanvas = document.createElement('canvas');
+ const backupCtx = backupCanvas.getContext('2d', {willReadFrequently:true});
+ backupCanvas.width = this.image.width;
+ backupCanvas.height = this.image.height;
+
+ backupCtx.clearRect(0,0, backupCanvas.width, backupCanvas.height);
+ backupCtx.drawImage(this.maskCanvas,
+ 0, 0, this.maskCanvas.width, this.maskCanvas.height,
+ 0, 0, backupCanvas.width, backupCanvas.height);
+
+ // paste mask data into alpha channel
+ const backupData = backupCtx.getImageData(0, 0, backupCanvas.width, backupCanvas.height);
+
+ // refine mask image
+ for (let i = 0; i < backupData.data.length; i += 4) {
+ if(backupData.data[i+3] == 255)
+ backupData.data[i+3] = 0;
+ else
+ backupData.data[i+3] = 255;
+
+ backupData.data[i] = 0;
+ backupData.data[i+1] = 0;
+ backupData.data[i+2] = 0;
+ }
+
+ backupCtx.globalCompositeOperation = 'source-over';
+ backupCtx.putImageData(backupData, 0, 0);
+
+ const formData = new FormData();
+ const filename = "clipspace-mask-" + performance.now() + ".png";
+
+ const item =
+ {
+ "filename": filename,
+ "subfolder": "clipspace",
+ "type": "input",
+ };
+
+ if(ComfyApp.clipspace.images)
+ ComfyApp.clipspace.images[0] = item;
+
+ if(ComfyApp.clipspace.widgets) {
+ const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');
+
+ if(index >= 0)
+ ComfyApp.clipspace.widgets[index].value = item;
+ }
+
+ const dataURL = backupCanvas.toDataURL();
+ const blob = dataURLToBlob(dataURL);
+
+ let original_url = new URL(this.image.src);
+
+ const original_ref = { filename: original_url.searchParams.get('filename') };
+
+ let original_subfolder = original_url.searchParams.get("subfolder");
+ if(original_subfolder)
+ original_ref.subfolder = original_subfolder;
+
+ let original_type = original_url.searchParams.get("type");
+ if(original_type)
+ original_ref.type = original_type;
+
+ formData.append('image', blob, filename);
+ formData.append('original_ref', JSON.stringify(original_ref));
+ formData.append('type', "input");
+ formData.append('subfolder', "clipspace");
+
+ this.saveButton.innerText = "Saving...";
+ this.saveButton.disabled = true;
+ await uploadMask(item, formData);
+ ComfyApp.onClipspaceEditorSave();
+ this.close();
+ }
+}
+
+app.registerExtension({
+ name: "Comfy.MaskEditor",
+ init(app) {
+ ComfyApp.open_maskeditor =
+ function () {
+ const dlg = MaskEditorDialog.getInstance();
+ if(!dlg.isOpened()) {
+ dlg.show();
+ }
+ };
+
+ const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0
+ ClipspaceDialog.registerButton("MaskEditor", context_predicate, ComfyApp.open_maskeditor);
+ }
+});
diff --git a/ComfyUI/web/extensions/core/nodeTemplates.js b/ComfyUI/web/extensions/core/nodeTemplates.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc9a108644ab6a080d1c88cab86c2b39296daf04
--- /dev/null
+++ b/ComfyUI/web/extensions/core/nodeTemplates.js
@@ -0,0 +1,374 @@
+import { app } from "../../scripts/app.js";
+import { ComfyDialog, $el } from "../../scripts/ui.js";
+import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js";
+
+// Adds the ability to save and add multiple nodes as a template
+// To save:
+// Select multiple nodes (ctrl + drag to select a region or ctrl+click individual nodes)
+// Right click the canvas
+// Save Node Template -> give it a name
+//
+// To add:
+// Right click the canvas
+// Node templates -> click the one to add
+//
+// To delete/rename:
+// Right click the canvas
+// Node templates -> Manage
+//
+// To rearrange:
+// Open the manage dialog and Drag and drop elements using the "Name:" label as handle
+
+const id = "Comfy.NodeTemplates";
+
+class ManageTemplates extends ComfyDialog {
+ constructor() {
+ super();
+ this.element.classList.add("comfy-manage-templates");
+ this.templates = this.load();
+ this.draggedEl = null;
+ this.saveVisualCue = null;
+ this.emptyImg = new Image();
+ this.emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
+
+ this.importInput = $el("input", {
+ type: "file",
+ accept: ".json",
+ multiple: true,
+ style: { display: "none" },
+ parent: document.body,
+ onchange: () => this.importAll(),
+ });
+ }
+
+ createButtons() {
+ const btns = super.createButtons();
+ btns[0].textContent = "Close";
+ btns[0].onclick = (e) => {
+ clearTimeout(this.saveVisualCue);
+ this.close();
+ };
+ btns.unshift(
+ $el("button", {
+ type: "button",
+ textContent: "Export",
+ onclick: () => this.exportAll(),
+ })
+ );
+ btns.unshift(
+ $el("button", {
+ type: "button",
+ textContent: "Import",
+ onclick: () => {
+ this.importInput.click();
+ },
+ })
+ );
+ return btns;
+ }
+
+ load() {
+ const templates = localStorage.getItem(id);
+ if (templates) {
+ return JSON.parse(templates);
+ } else {
+ return [];
+ }
+ }
+
+ store() {
+ localStorage.setItem(id, JSON.stringify(this.templates));
+ }
+
+ async importAll() {
+ for (const file of this.importInput.files) {
+ if (file.type === "application/json" || file.name.endsWith(".json")) {
+ const reader = new FileReader();
+ reader.onload = async () => {
+ var importFile = JSON.parse(reader.result);
+ if (importFile && importFile?.templates) {
+ for (const template of importFile.templates) {
+ if (template?.name && template?.data) {
+ this.templates.push(template);
+ }
+ }
+ this.store();
+ }
+ };
+ await reader.readAsText(file);
+ }
+ }
+
+ this.importInput.value = null;
+
+ this.close();
+ }
+
+ exportAll() {
+ if (this.templates.length == 0) {
+ alert("No templates to export.");
+ return;
+ }
+
+ const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string
+ const blob = new Blob([json], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+ const a = $el("a", {
+ href: url,
+ download: "node_templates.json",
+ style: { display: "none" },
+ parent: document.body,
+ });
+ a.click();
+ setTimeout(function () {
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ }, 0);
+ }
+
+ show() {
+ // Show list of template names + delete button
+ super.show(
+ $el(
+ "div",
+ {},
+ this.templates.flatMap((t,i) => {
+ let nameInput;
+ return [
+ $el(
+ "div",
+ {
+ dataset: { id: i },
+ className: "tempateManagerRow",
+ style: {
+ display: "grid",
+ gridTemplateColumns: "1fr auto",
+ border: "1px dashed transparent",
+ gap: "5px",
+ backgroundColor: "var(--comfy-menu-bg)"
+ },
+ ondragstart: (e) => {
+ this.draggedEl = e.currentTarget;
+ e.currentTarget.style.opacity = "0.6";
+ e.currentTarget.style.border = "1px dashed yellow";
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setDragImage(this.emptyImg, 0, 0);
+ },
+ ondragend: (e) => {
+ e.target.style.opacity = "1";
+ e.currentTarget.style.border = "1px dashed transparent";
+ e.currentTarget.removeAttribute("draggable");
+
+ // rearrange the elements in the localStorage
+ this.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => {
+ var prev_i = el.dataset.id;
+
+ if ( el == this.draggedEl && prev_i != i ) {
+ this.templates.splice(i, 0, this.templates.splice(prev_i, 1)[0]);
+ }
+ el.dataset.id = i;
+ });
+ this.store();
+ },
+ ondragover: (e) => {
+ e.preventDefault();
+ if ( e.currentTarget == this.draggedEl )
+ return;
+
+ let rect = e.currentTarget.getBoundingClientRect();
+ if (e.clientY > rect.top + rect.height / 2) {
+ e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget.nextSibling);
+ } else {
+ e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget);
+ }
+ }
+ },
+ [
+ $el(
+ "label",
+ {
+ textContent: "Name: ",
+ style: {
+ cursor: "grab",
+ },
+ onmousedown: (e) => {
+ // enable dragging only from the label
+ if (e.target.localName == 'label')
+ e.currentTarget.parentNode.draggable = 'true';
+ }
+ },
+ [
+ $el("input", {
+ value: t.name,
+ dataset: { name: t.name },
+ style: {
+ transitionProperty: 'background-color',
+ transitionDuration: '0s',
+ },
+ onchange: (e) => {
+ clearTimeout(this.saveVisualCue);
+ var el = e.target;
+ var row = el.parentNode.parentNode;
+ this.templates[row.dataset.id].name = el.value.trim() || 'untitled';
+ this.store();
+ el.style.backgroundColor = 'rgb(40, 95, 40)';
+ el.style.transitionDuration = '0s';
+ this.saveVisualCue = setTimeout(function () {
+ el.style.transitionDuration = '.7s';
+ el.style.backgroundColor = 'var(--comfy-input-bg)';
+ }, 15);
+ },
+ onkeypress: (e) => {
+ var el = e.target;
+ clearTimeout(this.saveVisualCue);
+ el.style.transitionDuration = '0s';
+ el.style.backgroundColor = 'var(--comfy-input-bg)';
+ },
+ $: (el) => (nameInput = el),
+ })
+ ]
+ ),
+ $el(
+ "div",
+ {},
+ [
+ $el("button", {
+ textContent: "Export",
+ style: {
+ fontSize: "12px",
+ fontWeight: "normal",
+ },
+ onclick: (e) => {
+ const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string
+ const blob = new Blob([json], {type: "application/json"});
+ const url = URL.createObjectURL(blob);
+ const a = $el("a", {
+ href: url,
+ download: (nameInput.value || t.name) + ".json",
+ style: {display: "none"},
+ parent: document.body,
+ });
+ a.click();
+ setTimeout(function () {
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ }, 0);
+ },
+ }),
+ $el("button", {
+ textContent: "Delete",
+ style: {
+ fontSize: "12px",
+ color: "red",
+ fontWeight: "normal",
+ },
+ onclick: (e) => {
+ const item = e.target.parentNode.parentNode;
+ item.parentNode.removeChild(item);
+ this.templates.splice(item.dataset.id*1, 1);
+ this.store();
+ // update the rows index, setTimeout ensures that the list is updated
+ var that = this;
+ setTimeout(function (){
+ that.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => {
+ el.dataset.id = i;
+ });
+ }, 0);
+ },
+ }),
+ ]
+ ),
+ ]
+ )
+ ];
+ })
+ )
+ );
+ }
+}
+
+app.registerExtension({
+ name: id,
+ setup() {
+ const manage = new ManageTemplates();
+
+ const clipboardAction = async (cb) => {
+ // We use the clipboard functions but dont want to overwrite the current user clipboard
+ // Restore it after we've run our callback
+ const old = localStorage.getItem("litegrapheditor_clipboard");
+ await cb();
+ localStorage.setItem("litegrapheditor_clipboard", old);
+ };
+
+ const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
+ LGraphCanvas.prototype.getCanvasMenuOptions = function () {
+ const options = orig.apply(this, arguments);
+
+ options.push(null);
+ options.push({
+ content: `Save Selected as Template`,
+ disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
+ callback: () => {
+ const name = prompt("Enter name");
+ if (!name?.trim()) return;
+
+ clipboardAction(() => {
+ app.canvas.copyToClipboard();
+ let data = localStorage.getItem("litegrapheditor_clipboard");
+ data = JSON.parse(data);
+ const nodeIds = Object.keys(app.canvas.selected_nodes);
+ for (let i = 0; i < nodeIds.length; i++) {
+ const node = app.graph.getNodeById(nodeIds[i]);
+ const nodeData = node?.constructor.nodeData;
+
+ let groupData = GroupNodeHandler.getGroupData(node);
+ if (groupData) {
+ groupData = groupData.nodeData;
+ if (!data.groupNodes) {
+ data.groupNodes = {};
+ }
+ data.groupNodes[nodeData.name] = groupData;
+ data.nodes[i].type = nodeData.name;
+ }
+ }
+
+ manage.templates.push({
+ name,
+ data: JSON.stringify(data),
+ });
+ manage.store();
+ });
+ },
+ });
+
+ // Map each template to a menu item
+ const subItems = manage.templates.map((t) => {
+ return {
+ content: t.name,
+ callback: () => {
+ clipboardAction(async () => {
+ const data = JSON.parse(t.data);
+ await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
+ localStorage.setItem("litegrapheditor_clipboard", t.data);
+ app.canvas.pasteFromClipboard();
+ });
+ },
+ };
+ });
+
+ subItems.push(null, {
+ content: "Manage",
+ callback: () => manage.show(),
+ });
+
+ options.push({
+ content: "Node Templates",
+ submenu: {
+ options: subItems,
+ },
+ });
+
+ return options;
+ };
+ },
+});
diff --git a/ComfyUI/web/extensions/core/noteNode.js b/ComfyUI/web/extensions/core/noteNode.js
new file mode 100644
index 0000000000000000000000000000000000000000..8d89054e9f6465e467acd72b609f53b292b87d70
--- /dev/null
+++ b/ComfyUI/web/extensions/core/noteNode.js
@@ -0,0 +1,41 @@
+import {app} from "../../scripts/app.js";
+import {ComfyWidgets} from "../../scripts/widgets.js";
+// Node that add notes to your project
+
+app.registerExtension({
+ name: "Comfy.NoteNode",
+ registerCustomNodes() {
+ class NoteNode {
+ color=LGraphCanvas.node_colors.yellow.color;
+ bgcolor=LGraphCanvas.node_colors.yellow.bgcolor;
+ groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
+ constructor() {
+ if (!this.properties) {
+ this.properties = {};
+ this.properties.text="";
+ }
+
+ ComfyWidgets.STRING(this, "", ["", {default:this.properties.text, multiline: true}], app)
+
+ this.serialize_widgets = true;
+ this.isVirtualNode = true;
+
+ }
+
+
+ }
+
+ // Load default visibility
+
+ LiteGraph.registerNodeType(
+ "Note",
+ Object.assign(NoteNode, {
+ title_mode: LiteGraph.NORMAL_TITLE,
+ title: "Note",
+ collapsable: true,
+ })
+ );
+
+ NoteNode.category = "utils";
+ },
+});
diff --git a/ComfyUI/web/extensions/core/rerouteNode.js b/ComfyUI/web/extensions/core/rerouteNode.js
new file mode 100644
index 0000000000000000000000000000000000000000..4feff91e50e025b2d4dbaf08581e38110f397e74
--- /dev/null
+++ b/ComfyUI/web/extensions/core/rerouteNode.js
@@ -0,0 +1,274 @@
+import { app } from "../../scripts/app.js";
+import { mergeIfValid, getWidgetConfig, setWidgetConfig } from "./widgetInputs.js";
+
+// Node that allows you to redirect connections for cleaner graphs
+
+app.registerExtension({
+ name: "Comfy.RerouteNode",
+ registerCustomNodes(app) {
+ class RerouteNode {
+ constructor() {
+ if (!this.properties) {
+ this.properties = {};
+ }
+ this.properties.showOutputText = RerouteNode.defaultVisibility;
+ this.properties.horizontal = false;
+
+ this.addInput("", "*");
+ this.addOutput(this.properties.showOutputText ? "*" : "", "*");
+
+ this.onAfterGraphConfigured = function () {
+ requestAnimationFrame(() => {
+ this.onConnectionsChange(LiteGraph.INPUT, null, true, null);
+ });
+ };
+
+ this.onConnectionsChange = function (type, index, connected, link_info) {
+ this.applyOrientation();
+
+ // Prevent multiple connections to different types when we have no input
+ if (connected && type === LiteGraph.OUTPUT) {
+ // Ignore wildcard nodes as these will be updated to real types
+ const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*"));
+ if (types.size > 1) {
+ const linksToDisconnect = [];
+ for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
+ const linkId = this.outputs[0].links[i];
+ const link = app.graph.links[linkId];
+ linksToDisconnect.push(link);
+ }
+ for (const link of linksToDisconnect) {
+ const node = app.graph.getNodeById(link.target_id);
+ node.disconnectInput(link.target_slot);
+ }
+ }
+ }
+
+ // Find root input
+ let currentNode = this;
+ let updateNodes = [];
+ let inputType = null;
+ let inputNode = null;
+ while (currentNode) {
+ updateNodes.unshift(currentNode);
+ const linkId = currentNode.inputs[0].link;
+ if (linkId !== null) {
+ const link = app.graph.links[linkId];
+ if (!link) return;
+ const node = app.graph.getNodeById(link.origin_id);
+ const type = node.constructor.type;
+ if (type === "Reroute") {
+ if (node === this) {
+ // We've found a circle
+ currentNode.disconnectInput(link.target_slot);
+ currentNode = null;
+ } else {
+ // Move the previous node
+ currentNode = node;
+ }
+ } else {
+ // We've found the end
+ inputNode = currentNode;
+ inputType = node.outputs[link.origin_slot]?.type ?? null;
+ break;
+ }
+ } else {
+ // This path has no input node
+ currentNode = null;
+ break;
+ }
+ }
+
+ // Find all outputs
+ const nodes = [this];
+ let outputType = null;
+ while (nodes.length) {
+ currentNode = nodes.pop();
+ const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
+ if (outputs.length) {
+ for (const linkId of outputs) {
+ const link = app.graph.links[linkId];
+
+ // When disconnecting sometimes the link is still registered
+ if (!link) continue;
+
+ const node = app.graph.getNodeById(link.target_id);
+ const type = node.constructor.type;
+
+ if (type === "Reroute") {
+ // Follow reroute nodes
+ nodes.push(node);
+ updateNodes.push(node);
+ } else {
+ // We've found an output
+ const nodeOutType =
+ node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type
+ ? node.inputs[link.target_slot].type
+ : null;
+ if (inputType && inputType !== "*" && nodeOutType !== inputType) {
+ // The output doesnt match our input so disconnect it
+ node.disconnectInput(link.target_slot);
+ } else {
+ outputType = nodeOutType;
+ }
+ }
+ }
+ } else {
+ // No more outputs for this path
+ }
+ }
+
+ const displayType = inputType || outputType || "*";
+ const color = LGraphCanvas.link_type_colors[displayType];
+
+ let widgetConfig;
+ let targetWidget;
+ let widgetType;
+ // Update the types of each node
+ for (const node of updateNodes) {
+ // If we dont have an input type we are always wildcard but we'll show the output type
+ // This lets you change the output link to a different type and all nodes will update
+ node.outputs[0].type = inputType || "*";
+ node.__outputType = displayType;
+ node.outputs[0].name = node.properties.showOutputText ? displayType : "";
+ node.size = node.computeSize();
+ node.applyOrientation();
+
+ for (const l of node.outputs[0].links || []) {
+ const link = app.graph.links[l];
+ if (link) {
+ link.color = color;
+
+ if (app.configuringGraph) continue;
+ const targetNode = app.graph.getNodeById(link.target_id);
+ const targetInput = targetNode.inputs?.[link.target_slot];
+ if (targetInput?.widget) {
+ const config = getWidgetConfig(targetInput);
+ if (!widgetConfig) {
+ widgetConfig = config[1] ?? {};
+ widgetType = config[0];
+ }
+ if (!targetWidget) {
+ targetWidget = targetNode.widgets?.find((w) => w.name === targetInput.widget.name);
+ }
+
+ const merged = mergeIfValid(targetInput, [config[0], widgetConfig]);
+ if (merged.customConfig) {
+ widgetConfig = merged.customConfig;
+ }
+ }
+ }
+ }
+ }
+
+ for (const node of updateNodes) {
+ if (widgetConfig && outputType) {
+ node.inputs[0].widget = { name: "value" };
+ setWidgetConfig(node.inputs[0], [widgetType ?? displayType, widgetConfig], targetWidget);
+ } else {
+ setWidgetConfig(node.inputs[0], null);
+ }
+ }
+
+ if (inputNode) {
+ const link = app.graph.links[inputNode.inputs[0].link];
+ if (link) {
+ link.color = color;
+ }
+ }
+ };
+
+ this.clone = function () {
+ const cloned = RerouteNode.prototype.clone.apply(this);
+ cloned.removeOutput(0);
+ cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
+ cloned.size = cloned.computeSize();
+ return cloned;
+ };
+
+ // This node is purely frontend and does not impact the resulting prompt so should not be serialized
+ this.isVirtualNode = true;
+ }
+
+ getExtraMenuOptions(_, options) {
+ options.unshift(
+ {
+ content: (this.properties.showOutputText ? "Hide" : "Show") + " Type",
+ callback: () => {
+ this.properties.showOutputText = !this.properties.showOutputText;
+ if (this.properties.showOutputText) {
+ this.outputs[0].name = this.__outputType || this.outputs[0].type;
+ } else {
+ this.outputs[0].name = "";
+ }
+ this.size = this.computeSize();
+ this.applyOrientation();
+ app.graph.setDirtyCanvas(true, true);
+ },
+ },
+ {
+ content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default",
+ callback: () => {
+ RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility);
+ },
+ },
+ {
+ // naming is inverted with respect to LiteGraphNode.horizontal
+ // LiteGraphNode.horizontal == true means that
+ // each slot in the inputs and outputs are layed out horizontally,
+ // which is the opposite of the visual orientation of the inputs and outputs as a node
+ content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"),
+ callback: () => {
+ this.properties.horizontal = !this.properties.horizontal;
+ this.applyOrientation();
+ },
+ }
+ );
+ }
+ applyOrientation() {
+ this.horizontal = this.properties.horizontal;
+ if (this.horizontal) {
+ // we correct the input position, because LiteGraphNode.horizontal
+ // doesn't account for title presence
+ // which reroute nodes don't have
+ this.inputs[0].pos = [this.size[0] / 2, 0];
+ } else {
+ delete this.inputs[0].pos;
+ }
+ app.graph.setDirtyCanvas(true, true);
+ }
+
+ computeSize() {
+ return [
+ this.properties.showOutputText && this.outputs && this.outputs.length
+ ? Math.max(75, LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40)
+ : 75,
+ 26,
+ ];
+ }
+
+ static setDefaultTextVisibility(visible) {
+ RerouteNode.defaultVisibility = visible;
+ if (visible) {
+ localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true";
+ } else {
+ delete localStorage["Comfy.RerouteNode.DefaultVisibility"];
+ }
+ }
+ }
+
+ // Load default visibility
+ RerouteNode.setDefaultTextVisibility(!!localStorage["Comfy.RerouteNode.DefaultVisibility"]);
+
+ LiteGraph.registerNodeType(
+ "Reroute",
+ Object.assign(RerouteNode, {
+ title_mode: LiteGraph.NO_TITLE,
+ title: "Reroute",
+ collapsable: false,
+ })
+ );
+
+ RerouteNode.category = "utils";
+ },
+});
diff --git a/ComfyUI/web/extensions/core/saveImageExtraOutput.js b/ComfyUI/web/extensions/core/saveImageExtraOutput.js
new file mode 100644
index 0000000000000000000000000000000000000000..a0506b43b6b521251ebdca0cbb69788d3b503be6
--- /dev/null
+++ b/ComfyUI/web/extensions/core/saveImageExtraOutput.js
@@ -0,0 +1,35 @@
+import { app } from "../../scripts/app.js";
+import { applyTextReplacements } from "../../scripts/utils.js";
+// Use widget values and dates in output filenames
+
+app.registerExtension({
+ name: "Comfy.SaveImageExtraOutput",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === "SaveImage") {
+ const onNodeCreated = nodeType.prototype.onNodeCreated;
+ // When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
+ nodeType.prototype.onNodeCreated = function () {
+ const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
+
+ const widget = this.widgets.find((w) => w.name === "filename_prefix");
+ widget.serializeValue = () => {
+ return applyTextReplacements(app, widget.value);
+ };
+
+ return r;
+ };
+ } else {
+ // When any other node is created add a property to alias the node
+ const onNodeCreated = nodeType.prototype.onNodeCreated;
+ nodeType.prototype.onNodeCreated = function () {
+ const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
+
+ if (!this.properties || !("Node name for S&R" in this.properties)) {
+ this.addProperty("Node name for S&R", this.constructor.type, "string");
+ }
+
+ return r;
+ };
+ }
+ },
+});
diff --git a/ComfyUI/web/extensions/core/slotDefaults.js b/ComfyUI/web/extensions/core/slotDefaults.js
new file mode 100644
index 0000000000000000000000000000000000000000..718d25405713ba2d6c341424f113f9a58c5d965f
--- /dev/null
+++ b/ComfyUI/web/extensions/core/slotDefaults.js
@@ -0,0 +1,91 @@
+import { app } from "../../scripts/app.js";
+import { ComfyWidgets } from "../../scripts/widgets.js";
+// Adds defaults for quickly adding nodes with middle click on the input/output
+
+app.registerExtension({
+ name: "Comfy.SlotDefaults",
+ suggestionsNumber: null,
+ init() {
+ LiteGraph.search_filter_enabled = true;
+ LiteGraph.middle_click_slot_add_default_node = true;
+ this.suggestionsNumber = app.ui.settings.addSetting({
+ id: "Comfy.NodeSuggestions.number",
+ name: "Number of nodes suggestions",
+ type: "slider",
+ attrs: {
+ min: 1,
+ max: 100,
+ step: 1,
+ },
+ defaultValue: 5,
+ onChange: (newVal, oldVal) => {
+ this.setDefaults(newVal);
+ }
+ });
+ },
+ slot_types_default_out: {},
+ slot_types_default_in: {},
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ var nodeId = nodeData.name;
+ var inputs = [];
+ inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs
+ for (const inputKey in inputs) {
+ var input = (inputs[inputKey]);
+ if (typeof input[0] !== "string") continue;
+
+ var type = input[0]
+ if (type in ComfyWidgets) {
+ var customProperties = input[1]
+ if (!(customProperties?.forceInput)) continue; //ignore widgets that don't force input
+ }
+
+ if (!(type in this.slot_types_default_out)) {
+ this.slot_types_default_out[type] = ["Reroute"];
+ }
+ if (this.slot_types_default_out[type].includes(nodeId)) continue;
+ this.slot_types_default_out[type].push(nodeId);
+
+ // Input types have to be stored as lower case
+ // Store each node that can handle this input type
+ const lowerType = type.toLocaleLowerCase();
+ if (!(lowerType in LiteGraph.registered_slot_in_types)) {
+ LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] };
+ }
+ LiteGraph.registered_slot_in_types[lowerType].nodes.push(nodeType.comfyClass);
+ }
+
+ var outputs = nodeData["output"];
+ for (const key in outputs) {
+ var type = outputs[key];
+ if (!(type in this.slot_types_default_in)) {
+ this.slot_types_default_in[type] = ["Reroute"];// ["Reroute", "Primitive"]; primitive doesn't always work :'()
+ }
+
+ this.slot_types_default_in[type].push(nodeId);
+
+ // Store each node that can handle this output type
+ if (!(type in LiteGraph.registered_slot_out_types)) {
+ LiteGraph.registered_slot_out_types[type] = { nodes: [] };
+ }
+ LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass);
+
+ if(!LiteGraph.slot_types_out.includes(type)) {
+ LiteGraph.slot_types_out.push(type);
+ }
+ }
+ var maxNum = this.suggestionsNumber.value;
+ this.setDefaults(maxNum);
+ },
+ setDefaults(maxNum) {
+
+ LiteGraph.slot_types_default_out = {};
+ LiteGraph.slot_types_default_in = {};
+
+ for (const type in this.slot_types_default_out) {
+ LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[type].slice(0, maxNum);
+ }
+ for (const type in this.slot_types_default_in) {
+ LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[type].slice(0, maxNum);
+ }
+ }
+});
diff --git a/ComfyUI/web/extensions/core/snapToGrid.js b/ComfyUI/web/extensions/core/snapToGrid.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc534d6edf97a3d20a51b7ca5dc6d5fde770ef5a
--- /dev/null
+++ b/ComfyUI/web/extensions/core/snapToGrid.js
@@ -0,0 +1,89 @@
+import { app } from "../../scripts/app.js";
+
+// Shift + drag/resize to snap to grid
+
+app.registerExtension({
+ name: "Comfy.SnapToGrid",
+ init() {
+ // Add setting to control grid size
+ app.ui.settings.addSetting({
+ id: "Comfy.SnapToGrid.GridSize",
+ name: "Grid Size",
+ type: "slider",
+ attrs: {
+ min: 1,
+ max: 500,
+ },
+ tooltip:
+ "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.",
+ defaultValue: LiteGraph.CANVAS_GRID_SIZE,
+ onChange(value) {
+ LiteGraph.CANVAS_GRID_SIZE = +value;
+ },
+ });
+
+ // After moving a node, if the shift key is down align it to grid
+ const onNodeMoved = app.canvas.onNodeMoved;
+ app.canvas.onNodeMoved = function (node) {
+ const r = onNodeMoved?.apply(this, arguments);
+
+ if (app.shiftDown) {
+ // Ensure all selected nodes are realigned
+ for (const id in this.selected_nodes) {
+ this.selected_nodes[id].alignToGrid();
+ }
+ }
+
+ return r;
+ };
+
+ // When a node is added, add a resize handler to it so we can fix align the size with the grid
+ const onNodeAdded = app.graph.onNodeAdded;
+ app.graph.onNodeAdded = function (node) {
+ const onResize = node.onResize;
+ node.onResize = function () {
+ if (app.shiftDown) {
+ const w = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.size[0] / LiteGraph.CANVAS_GRID_SIZE);
+ const h = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.size[1] / LiteGraph.CANVAS_GRID_SIZE);
+ node.size[0] = w;
+ node.size[1] = h;
+ }
+ return onResize?.apply(this, arguments);
+ };
+ return onNodeAdded?.apply(this, arguments);
+ };
+
+ // Draw a preview of where the node will go if holding shift and the node is selected
+ const origDrawNode = LGraphCanvas.prototype.drawNode;
+ LGraphCanvas.prototype.drawNode = function (node, ctx) {
+ if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) {
+ const x = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[0] / LiteGraph.CANVAS_GRID_SIZE);
+ const y = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[1] / LiteGraph.CANVAS_GRID_SIZE);
+
+ const shiftX = x - node.pos[0];
+ let shiftY = y - node.pos[1];
+
+ let w, h;
+ if (node.flags.collapsed) {
+ w = node._collapsed_width;
+ h = LiteGraph.NODE_TITLE_HEIGHT;
+ shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
+ } else {
+ w = node.size[0];
+ h = node.size[1];
+ let titleMode = node.constructor.title_mode;
+ if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) {
+ h += LiteGraph.NODE_TITLE_HEIGHT;
+ shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
+ }
+ }
+ const f = ctx.fillStyle;
+ ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
+ ctx.fillRect(shiftX, shiftY, w, h);
+ ctx.fillStyle = f;
+ }
+
+ return origDrawNode.apply(this, arguments);
+ };
+ },
+});
diff --git a/ComfyUI/web/extensions/core/undoRedo.js b/ComfyUI/web/extensions/core/undoRedo.js
new file mode 100644
index 0000000000000000000000000000000000000000..3cb137520f4bb14847fc4db694afbc21dfd465e9
--- /dev/null
+++ b/ComfyUI/web/extensions/core/undoRedo.js
@@ -0,0 +1,147 @@
+import { app } from "../../scripts/app.js";
+
+const MAX_HISTORY = 50;
+
+let undo = [];
+let redo = [];
+let activeState = null;
+let isOurLoad = false;
+function checkState() {
+ const currentState = app.graph.serialize();
+ if (!graphEqual(activeState, currentState)) {
+ undo.push(activeState);
+ if (undo.length > MAX_HISTORY) {
+ undo.shift();
+ }
+ activeState = clone(currentState);
+ redo.length = 0;
+ }
+}
+
+const loadGraphData = app.loadGraphData;
+app.loadGraphData = async function () {
+ const v = await loadGraphData.apply(this, arguments);
+ if (isOurLoad) {
+ isOurLoad = false;
+ } else {
+ checkState();
+ }
+ return v;
+};
+
+function clone(obj) {
+ try {
+ if (typeof structuredClone !== "undefined") {
+ return structuredClone(obj);
+ }
+ } catch (error) {
+ // structuredClone is stricter than using JSON.parse/stringify so fallback to that
+ }
+
+ return JSON.parse(JSON.stringify(obj));
+}
+
+function graphEqual(a, b, root = true) {
+ if (a === b) return true;
+
+ if (typeof a == "object" && a && typeof b == "object" && b) {
+ const keys = Object.getOwnPropertyNames(a);
+
+ if (keys.length != Object.getOwnPropertyNames(b).length) {
+ return false;
+ }
+
+ for (const key of keys) {
+ let av = a[key];
+ let bv = b[key];
+ if (root && key === "nodes") {
+ // Nodes need to be sorted as the order changes when selecting nodes
+ av = [...av].sort((a, b) => a.id - b.id);
+ bv = [...bv].sort((a, b) => a.id - b.id);
+ }
+ if (!graphEqual(av, bv, false)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+const undoRedo = async (e) => {
+ const updateState = async (source, target) => {
+ const prevState = source.pop();
+ if (prevState) {
+ target.push(activeState);
+ isOurLoad = true;
+ await app.loadGraphData(prevState, false);
+ activeState = prevState;
+ }
+ }
+ if (e.ctrlKey || e.metaKey) {
+ if (e.key === "y") {
+ updateState(redo, undo);
+ return true;
+ } else if (e.key === "z") {
+ updateState(undo, redo);
+ return true;
+ }
+ }
+};
+
+const bindInput = (activeEl) => {
+ if (activeEl?.tagName !== "CANVAS" && activeEl?.tagName !== "BODY") {
+ for (const evt of ["change", "input", "blur"]) {
+ if (`on${evt}` in activeEl) {
+ const listener = () => {
+ checkState();
+ activeEl.removeEventListener(evt, listener);
+ };
+ activeEl.addEventListener(evt, listener);
+ return true;
+ }
+ }
+ }
+};
+
+window.addEventListener(
+ "keydown",
+ (e) => {
+ requestAnimationFrame(async () => {
+ const activeEl = document.activeElement;
+ if (activeEl?.tagName === "INPUT" || activeEl?.type === "textarea") {
+ // Ignore events on inputs, they have their native history
+ return;
+ }
+
+ // Check if this is a ctrl+z ctrl+y
+ if (await undoRedo(e)) return;
+
+ // If our active element is some type of input then handle changes after they're done
+ if (bindInput(activeEl)) return;
+ checkState();
+ });
+ },
+ true
+);
+
+// Handle clicking DOM elements (e.g. widgets)
+window.addEventListener("mouseup", () => {
+ checkState();
+});
+
+// Handle litegraph clicks
+const processMouseUp = LGraphCanvas.prototype.processMouseUp;
+LGraphCanvas.prototype.processMouseUp = function (e) {
+ const v = processMouseUp.apply(this, arguments);
+ checkState();
+ return v;
+};
+const processMouseDown = LGraphCanvas.prototype.processMouseDown;
+LGraphCanvas.prototype.processMouseDown = function (e) {
+ const v = processMouseDown.apply(this, arguments);
+ checkState();
+ return v;
+};
diff --git a/ComfyUI/web/extensions/core/uploadImage.js b/ComfyUI/web/extensions/core/uploadImage.js
new file mode 100644
index 0000000000000000000000000000000000000000..530c4599e7990eb619af3b3dabacecec0a0e4334
--- /dev/null
+++ b/ComfyUI/web/extensions/core/uploadImage.js
@@ -0,0 +1,12 @@
+import { app } from "../../scripts/app.js";
+
+// Adds an upload button to the nodes
+
+app.registerExtension({
+ name: "Comfy.UploadImage",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData?.input?.required?.image?.[1]?.image_upload === true) {
+ nodeData.input.required.upload = ["IMAGEUPLOAD"];
+ }
+ },
+});
diff --git a/ComfyUI/web/extensions/core/widgetInputs.js b/ComfyUI/web/extensions/core/widgetInputs.js
new file mode 100644
index 0000000000000000000000000000000000000000..3f1c1f8c126fac0130751b80c42aee988b01a058
--- /dev/null
+++ b/ComfyUI/web/extensions/core/widgetInputs.js
@@ -0,0 +1,764 @@
+import { ComfyWidgets, addValueControlWidgets } from "../../scripts/widgets.js";
+import { app } from "../../scripts/app.js";
+import { applyTextReplacements } from "../../scripts/utils.js";
+
+const CONVERTED_TYPE = "converted-widget";
+const VALID_TYPES = ["STRING", "combo", "number", "BOOLEAN"];
+const CONFIG = Symbol();
+const GET_CONFIG = Symbol();
+const TARGET = Symbol(); // Used for reroutes to specify the real target widget
+
+export function getWidgetConfig(slot) {
+ return slot.widget[CONFIG] ?? slot.widget[GET_CONFIG]();
+}
+
+function getConfig(widgetName) {
+ const { nodeData } = this.constructor;
+ return nodeData?.input?.required[widgetName] ?? nodeData?.input?.optional?.[widgetName];
+}
+
+function isConvertableWidget(widget, config) {
+ return (VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) && !widget.options?.forceInput;
+}
+
+function hideWidget(node, widget, suffix = "") {
+ widget.origType = widget.type;
+ widget.origComputeSize = widget.computeSize;
+ widget.origSerializeValue = widget.serializeValue;
+ widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
+ widget.type = CONVERTED_TYPE + suffix;
+ widget.serializeValue = () => {
+ // Prevent serializing the widget if we have no input linked
+ if (!node.inputs) {
+ return undefined;
+ }
+ let node_input = node.inputs.find((i) => i.widget?.name === widget.name);
+
+ if (!node_input || !node_input.link) {
+ return undefined;
+ }
+ return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
+ };
+
+ // Hide any linked widgets, e.g. seed+seedControl
+ if (widget.linkedWidgets) {
+ for (const w of widget.linkedWidgets) {
+ hideWidget(node, w, ":" + widget.name);
+ }
+ }
+}
+
+function showWidget(widget) {
+ widget.type = widget.origType;
+ widget.computeSize = widget.origComputeSize;
+ widget.serializeValue = widget.origSerializeValue;
+
+ delete widget.origType;
+ delete widget.origComputeSize;
+ delete widget.origSerializeValue;
+
+ // Hide any linked widgets, e.g. seed+seedControl
+ if (widget.linkedWidgets) {
+ for (const w of widget.linkedWidgets) {
+ showWidget(w);
+ }
+ }
+}
+
+function convertToInput(node, widget, config) {
+ hideWidget(node, widget);
+
+ const { type } = getWidgetType(config);
+
+ // Add input and store widget config for creating on primitive node
+ const sz = node.size;
+ node.addInput(widget.name, type, {
+ widget: { name: widget.name, [GET_CONFIG]: () => config },
+ });
+
+ for (const widget of node.widgets) {
+ widget.last_y += LiteGraph.NODE_SLOT_HEIGHT;
+ }
+
+ // Restore original size but grow if needed
+ node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
+}
+
+function convertToWidget(node, widget) {
+ showWidget(widget);
+ const sz = node.size;
+ node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name));
+
+ for (const widget of node.widgets) {
+ widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT;
+ }
+
+ // Restore original size but grow if needed
+ node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
+}
+
+function getWidgetType(config) {
+ // Special handling for COMBO so we restrict links based on the entries
+ let type = config[0];
+ if (type instanceof Array) {
+ type = "COMBO";
+ }
+ return { type };
+}
+
+function isValidCombo(combo, obj) {
+ // New input isnt a combo
+ if (!(obj instanceof Array)) {
+ console.log(`connection rejected: tried to connect combo to ${obj}`);
+ return false;
+ }
+ // New imput combo has a different size
+ if (combo.length !== obj.length) {
+ console.log(`connection rejected: combo lists dont match`);
+ return false;
+ }
+ // New input combo has different elements
+ if (combo.find((v, i) => obj[i] !== v)) {
+ console.log(`connection rejected: combo lists dont match`);
+ return false;
+ }
+
+ return true;
+}
+
+export function setWidgetConfig(slot, config, target) {
+ if (!slot.widget) return;
+ if (config) {
+ slot.widget[GET_CONFIG] = () => config;
+ slot.widget[TARGET] = target;
+ } else {
+ delete slot.widget;
+ }
+
+ if (slot.link) {
+ const link = app.graph.links[slot.link];
+ if (link) {
+ const originNode = app.graph.getNodeById(link.origin_id);
+ if (originNode.type === "PrimitiveNode") {
+ if (config) {
+ originNode.recreateWidget();
+ } else if(!app.configuringGraph) {
+ originNode.disconnectOutput(0);
+ originNode.onLastDisconnect();
+ }
+ }
+ }
+ }
+}
+
+export function mergeIfValid(output, config2, forceUpdate, recreateWidget, config1) {
+ if (!config1) {
+ config1 = output.widget[CONFIG] ?? output.widget[GET_CONFIG]();
+ }
+
+ if (config1[0] instanceof Array) {
+ if (!isValidCombo(config1[0], config2[0])) return false;
+ } else if (config1[0] !== config2[0]) {
+ // Types dont match
+ console.log(`connection rejected: types dont match`, config1[0], config2[0]);
+ return false;
+ }
+
+ const keys = new Set([...Object.keys(config1[1] ?? {}), ...Object.keys(config2[1] ?? {})]);
+
+ let customConfig;
+ const getCustomConfig = () => {
+ if (!customConfig) {
+ if (typeof structuredClone === "undefined") {
+ customConfig = JSON.parse(JSON.stringify(config1[1] ?? {}));
+ } else {
+ customConfig = structuredClone(config1[1] ?? {});
+ }
+ }
+ return customConfig;
+ };
+
+ const isNumber = config1[0] === "INT" || config1[0] === "FLOAT";
+ for (const k of keys.values()) {
+ if (k !== "default" && k !== "forceInput" && k !== "defaultInput" && k !== "control_after_generate" && k !== "multiline") {
+ let v1 = config1[1][k];
+ let v2 = config2[1]?.[k];
+
+ if (v1 === v2 || (!v1 && !v2)) continue;
+
+ if (isNumber) {
+ if (k === "min") {
+ const theirMax = config2[1]?.["max"];
+ if (theirMax != null && v1 > theirMax) {
+ console.log("connection rejected: min > max", v1, theirMax);
+ return false;
+ }
+ getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.max(v1, v2);
+ continue;
+ } else if (k === "max") {
+ const theirMin = config2[1]?.["min"];
+ if (theirMin != null && v1 < theirMin) {
+ console.log("connection rejected: max < min", v1, theirMin);
+ return false;
+ }
+ getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2);
+ continue;
+ } else if (k === "step") {
+ let step;
+ if (v1 == null) {
+ // No current step
+ step = v2;
+ } else if (v2 == null) {
+ // No new step
+ step = v1;
+ } else {
+ if (v1 < v2) {
+ // Ensure v1 is larger for the mod
+ const a = v2;
+ v2 = v1;
+ v1 = a;
+ }
+ if (v1 % v2) {
+ console.log("connection rejected: steps not divisible", "current:", v1, "new:", v2);
+ return false;
+ }
+
+ step = v1;
+ }
+
+ getCustomConfig()[k] = step;
+ continue;
+ }
+ }
+
+ console.log(`connection rejected: config ${k} values dont match`, v1, v2);
+ return false;
+ }
+ }
+
+ if (customConfig || forceUpdate) {
+ if (customConfig) {
+ output.widget[CONFIG] = [config1[0], customConfig];
+ }
+
+ const widget = recreateWidget?.call(this);
+ // When deleting a node this can be null
+ if (widget) {
+ const min = widget.options.min;
+ const max = widget.options.max;
+ if (min != null && widget.value < min) widget.value = min;
+ if (max != null && widget.value > max) widget.value = max;
+ widget.callback(widget.value);
+ }
+ }
+
+ return { customConfig };
+}
+
+app.registerExtension({
+ name: "Comfy.WidgetInputs",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ // Add menu options to conver to/from widgets
+ const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
+ nodeType.prototype.getExtraMenuOptions = function (_, options) {
+ const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined;
+
+ if (this.widgets) {
+ let toInput = [];
+ let toWidget = [];
+ for (const w of this.widgets) {
+ if (w.options?.forceInput) {
+ continue;
+ }
+ if (w.type === CONVERTED_TYPE) {
+ toWidget.push({
+ content: `Convert ${w.name} to widget`,
+ callback: () => convertToWidget(this, w),
+ });
+ } else {
+ const config = getConfig.call(this, w.name) ?? [w.type, w.options || {}];
+ if (isConvertableWidget(w, config)) {
+ toInput.push({
+ content: `Convert ${w.name} to input`,
+ callback: () => convertToInput(this, w, config),
+ });
+ }
+ }
+ }
+ if (toInput.length) {
+ options.push(...toInput, null);
+ }
+
+ if (toWidget.length) {
+ options.push(...toWidget, null);
+ }
+ }
+
+ return r;
+ };
+
+ nodeType.prototype.onGraphConfigured = function () {
+ if (!this.inputs) return;
+
+ for (const input of this.inputs) {
+ if (input.widget) {
+ if (!input.widget[GET_CONFIG]) {
+ input.widget[GET_CONFIG] = () => getConfig.call(this, input.widget.name);
+ }
+
+ // Cleanup old widget config
+ if (input.widget.config) {
+ if (input.widget.config[0] instanceof Array) {
+ // If we are an old converted combo then replace the input type and the stored link data
+ input.type = "COMBO";
+
+ const link = app.graph.links[input.link];
+ if (link) {
+ link.type = input.type;
+ }
+ }
+ delete input.widget.config;
+ }
+
+ const w = this.widgets.find((w) => w.name === input.widget.name);
+ if (w) {
+ hideWidget(this, w);
+ } else {
+ convertToWidget(this, input);
+ }
+ }
+ }
+ };
+
+ const origOnNodeCreated = nodeType.prototype.onNodeCreated;
+ nodeType.prototype.onNodeCreated = function () {
+ const r = origOnNodeCreated ? origOnNodeCreated.apply(this) : undefined;
+
+ // When node is created, convert any force/default inputs
+ if (!app.configuringGraph && this.widgets) {
+ for (const w of this.widgets) {
+ if (w?.options?.forceInput || w?.options?.defaultInput) {
+ const config = getConfig.call(this, w.name) ?? [w.type, w.options || {}];
+ convertToInput(this, w, config);
+ }
+ }
+ }
+
+ return r;
+ };
+
+ const origOnConfigure = nodeType.prototype.onConfigure;
+ nodeType.prototype.onConfigure = function () {
+ const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined;
+ if (!app.configuringGraph && this.inputs) {
+ // On copy + paste of nodes, ensure that widget configs are set up
+ for (const input of this.inputs) {
+ if (input.widget && !input.widget[GET_CONFIG]) {
+ input.widget[GET_CONFIG] = () => getConfig.call(this, input.widget.name);
+ const w = this.widgets.find((w) => w.name === input.widget.name);
+ if (w) {
+ hideWidget(this, w);
+ }
+ }
+ }
+ }
+
+ return r;
+ };
+
+ function isNodeAtPos(pos) {
+ for (const n of app.graph._nodes) {
+ if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Double click a widget input to automatically attach a primitive
+ const origOnInputDblClick = nodeType.prototype.onInputDblClick;
+ const ignoreDblClick = Symbol();
+ nodeType.prototype.onInputDblClick = function (slot) {
+ const r = origOnInputDblClick ? origOnInputDblClick.apply(this, arguments) : undefined;
+
+ const input = this.inputs[slot];
+ if (!input.widget || !input[ignoreDblClick]) {
+ // Not a widget input or already handled input
+ if (!(input.type in ComfyWidgets) && !(input.widget[GET_CONFIG]?.()?.[0] instanceof Array)) {
+ return r; //also Not a ComfyWidgets input or combo (do nothing)
+ }
+ }
+
+ // Create a primitive node
+ const node = LiteGraph.createNode("PrimitiveNode");
+ app.graph.add(node);
+
+ // Calculate a position that wont directly overlap another node
+ const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]];
+ while (isNodeAtPos(pos)) {
+ pos[1] += LiteGraph.NODE_TITLE_HEIGHT;
+ }
+
+ node.pos = pos;
+ node.connect(0, this, slot);
+ node.title = input.name;
+
+ // Prevent adding duplicates due to triple clicking
+ input[ignoreDblClick] = true;
+ setTimeout(() => {
+ delete input[ignoreDblClick];
+ }, 300);
+
+ return r;
+ };
+
+ // Prevent connecting COMBO lists to converted inputs that dont match types
+ const onConnectInput = nodeType.prototype.onConnectInput;
+ nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
+ const v = onConnectInput?.(this, arguments);
+ // Not a combo, ignore
+ if (type !== "COMBO") return v;
+ // Primitive output, allow that to handle
+ if (originNode.outputs[originSlot].widget) return v;
+
+ // Ensure target is also a combo
+ const targetCombo = this.inputs[targetSlot].widget?.[GET_CONFIG]?.()?.[0];
+ if (!targetCombo || !(targetCombo instanceof Array)) return v;
+
+ // Check they match
+ const originConfig = originNode.constructor?.nodeData?.output?.[originSlot];
+ if (!originConfig || !isValidCombo(targetCombo, originConfig)) {
+ return false;
+ }
+
+ return v;
+ };
+ },
+ registerCustomNodes() {
+ const replacePropertyName = "Run widget replace on values";
+ class PrimitiveNode {
+ constructor() {
+ this.addOutput("connect to widget input", "*");
+ this.serialize_widgets = true;
+ this.isVirtualNode = true;
+
+ if (!this.properties || !(replacePropertyName in this.properties)) {
+ this.addProperty(replacePropertyName, false, "boolean");
+ }
+ }
+
+ applyToGraph(extraLinks = []) {
+ if (!this.outputs[0].links?.length) return;
+
+ function get_links(node) {
+ let links = [];
+ for (const l of node.outputs[0].links) {
+ const linkInfo = app.graph.links[l];
+ const n = node.graph.getNodeById(linkInfo.target_id);
+ if (n.type == "Reroute") {
+ links = links.concat(get_links(n));
+ } else {
+ links.push(l);
+ }
+ }
+ return links;
+ }
+
+ let links = [...get_links(this).map((l) => app.graph.links[l]), ...extraLinks];
+ let v = this.widgets?.[0].value;
+ if(v && this.properties[replacePropertyName]) {
+ v = applyTextReplacements(app, v);
+ }
+
+ // For each output link copy our value over the original widget value
+ for (const linkInfo of links) {
+ const node = this.graph.getNodeById(linkInfo.target_id);
+ const input = node.inputs[linkInfo.target_slot];
+ let widget;
+ if (input.widget[TARGET]) {
+ widget = input.widget[TARGET];
+ } else {
+ const widgetName = input.widget.name;
+ if (widgetName) {
+ widget = node.widgets.find((w) => w.name === widgetName);
+ }
+ }
+
+ if (widget) {
+ widget.value = v;
+ if (widget.callback) {
+ widget.callback(widget.value, app.canvas, node, app.canvas.graph_mouse, {});
+ }
+ }
+ }
+ }
+
+ refreshComboInNode() {
+ const widget = this.widgets?.[0];
+ if (widget?.type === "combo") {
+ widget.options.values = this.outputs[0].widget[GET_CONFIG]()[0];
+
+ if (!widget.options.values.includes(widget.value)) {
+ widget.value = widget.options.values[0];
+ widget.callback(widget.value);
+ }
+ }
+ }
+
+ onAfterGraphConfigured() {
+ if (this.outputs[0].links?.length && !this.widgets?.length) {
+ if (!this.#onFirstConnection()) return;
+
+ // Populate widget values from config data
+ if (this.widgets) {
+ for (let i = 0; i < this.widgets_values.length; i++) {
+ const w = this.widgets[i];
+ if (w) {
+ w.value = this.widgets_values[i];
+ }
+ }
+ }
+
+ // Merge values if required
+ this.#mergeWidgetConfig();
+ }
+ }
+
+ onConnectionsChange(_, index, connected) {
+ if (app.configuringGraph) {
+ // Dont run while the graph is still setting up
+ return;
+ }
+
+ const links = this.outputs[0].links;
+ if (connected) {
+ if (links?.length && !this.widgets?.length) {
+ this.#onFirstConnection();
+ }
+ } else {
+ // We may have removed a link that caused the constraints to change
+ this.#mergeWidgetConfig();
+
+ if (!links?.length) {
+ this.onLastDisconnect();
+ }
+ }
+ }
+
+ onConnectOutput(slot, type, input, target_node, target_slot) {
+ // Fires before the link is made allowing us to reject it if it isn't valid
+ // No widget, we cant connect
+ if (!input.widget) {
+ if (!(input.type in ComfyWidgets)) return false;
+ }
+
+ if (this.outputs[slot].links?.length) {
+ const valid = this.#isValidConnection(input);
+ if (valid) {
+ // On connect of additional outputs, copy our value to their widget
+ this.applyToGraph([{ target_id: target_node.id, target_slot }]);
+ }
+ return valid;
+ }
+ }
+
+ #onFirstConnection(recreating) {
+ // First connection can fire before the graph is ready on initial load so random things can be missing
+ if (!this.outputs[0].links) {
+ this.onLastDisconnect();
+ return;
+ }
+ const linkId = this.outputs[0].links[0];
+ const link = this.graph.links[linkId];
+ if (!link) return;
+
+ const theirNode = this.graph.getNodeById(link.target_id);
+ if (!theirNode || !theirNode.inputs) return;
+
+ const input = theirNode.inputs[link.target_slot];
+ if (!input) return;
+
+ let widget;
+ if (!input.widget) {
+ if (!(input.type in ComfyWidgets)) return;
+ widget = { name: input.name, [GET_CONFIG]: () => [input.type, {}] }; //fake widget
+ } else {
+ widget = input.widget;
+ }
+
+ const config = widget[GET_CONFIG]?.();
+ if (!config) return;
+
+ const { type } = getWidgetType(config);
+ // Update our output to restrict to the widget type
+ this.outputs[0].type = type;
+ this.outputs[0].name = type;
+ this.outputs[0].widget = widget;
+
+ this.#createWidget(widget[CONFIG] ?? config, theirNode, widget.name, recreating, widget[TARGET]);
+ }
+
+ #createWidget(inputData, node, widgetName, recreating, targetWidget) {
+ let type = inputData[0];
+
+ if (type instanceof Array) {
+ type = "COMBO";
+ }
+
+ let widget;
+ if (type in ComfyWidgets) {
+ widget = (ComfyWidgets[type](this, "value", inputData, app) || {}).widget;
+ } else {
+ widget = this.addWidget(type, "value", null, () => {}, {});
+ }
+
+ if (targetWidget) {
+ widget.value = targetWidget.value;
+ } else if (node?.widgets && widget) {
+ const theirWidget = node.widgets.find((w) => w.name === widgetName);
+ if (theirWidget) {
+ widget.value = theirWidget.value;
+ }
+ }
+
+ if (!inputData?.[1]?.control_after_generate && (widget.type === "number" || widget.type === "combo")) {
+ let control_value = this.widgets_values?.[1];
+ if (!control_value) {
+ control_value = "fixed";
+ }
+ addValueControlWidgets(this, widget, control_value, undefined, inputData);
+ let filter = this.widgets_values?.[2];
+ if (filter && this.widgets.length === 3) {
+ this.widgets[2].value = filter;
+ }
+ }
+
+ // Restore any saved control values
+ const controlValues = this.controlValues;
+ if(this.lastType === this.widgets[0].type && controlValues?.length === this.widgets.length - 1) {
+ for(let i = 0; i < controlValues.length; i++) {
+ this.widgets[i + 1].value = controlValues[i];
+ }
+ }
+
+ // When our value changes, update other widgets to reflect our changes
+ // e.g. so LoadImage shows correct image
+ const callback = widget.callback;
+ const self = this;
+ widget.callback = function () {
+ const r = callback ? callback.apply(this, arguments) : undefined;
+ self.applyToGraph();
+ return r;
+ };
+
+ if (!recreating) {
+ // Grow our node if required
+ const sz = this.computeSize();
+ if (this.size[0] < sz[0]) {
+ this.size[0] = sz[0];
+ }
+ if (this.size[1] < sz[1]) {
+ this.size[1] = sz[1];
+ }
+
+ requestAnimationFrame(() => {
+ if (this.onResize) {
+ this.onResize(this.size);
+ }
+ });
+ }
+ }
+
+ recreateWidget() {
+ const values = this.widgets?.map((w) => w.value);
+ this.#removeWidgets();
+ this.#onFirstConnection(true);
+ if (values?.length) {
+ for (let i = 0; i < this.widgets?.length; i++) this.widgets[i].value = values[i];
+ }
+ return this.widgets?.[0];
+ }
+
+ #mergeWidgetConfig() {
+ // Merge widget configs if the node has multiple outputs
+ const output = this.outputs[0];
+ const links = output.links;
+
+ const hasConfig = !!output.widget[CONFIG];
+ if (hasConfig) {
+ delete output.widget[CONFIG];
+ }
+
+ if (links?.length < 2 && hasConfig) {
+ // Copy the widget options from the source
+ if (links.length) {
+ this.recreateWidget();
+ }
+
+ return;
+ }
+
+ const config1 = output.widget[GET_CONFIG]();
+ const isNumber = config1[0] === "INT" || config1[0] === "FLOAT";
+ if (!isNumber) return;
+
+ for (const linkId of links) {
+ const link = app.graph.links[linkId];
+ if (!link) continue; // Can be null when removing a node
+
+ const theirNode = app.graph.getNodeById(link.target_id);
+ const theirInput = theirNode.inputs[link.target_slot];
+
+ // Call is valid connection so it can merge the configs when validating
+ this.#isValidConnection(theirInput, hasConfig);
+ }
+ }
+
+ #isValidConnection(input, forceUpdate) {
+ // Only allow connections where the configs match
+ const output = this.outputs[0];
+ const config2 = input.widget[GET_CONFIG]();
+ return !!mergeIfValid.call(this, output, config2, forceUpdate, this.recreateWidget);
+ }
+
+ #removeWidgets() {
+ if (this.widgets) {
+ // Allow widgets to cleanup
+ for (const w of this.widgets) {
+ if (w.onRemove) {
+ w.onRemove();
+ }
+ }
+
+ // Temporarily store the current values in case the node is being recreated
+ // e.g. by group node conversion
+ this.controlValues = [];
+ this.lastType = this.widgets[0]?.type;
+ for(let i = 1; i < this.widgets.length; i++) {
+ this.controlValues.push(this.widgets[i].value);
+ }
+ setTimeout(() => { delete this.lastType; delete this.controlValues }, 15);
+ this.widgets.length = 0;
+ }
+ }
+
+ onLastDisconnect() {
+ // We cant remove + re-add the output here as if you drag a link over the same link
+ // it removes, then re-adds, causing it to break
+ this.outputs[0].type = "*";
+ this.outputs[0].name = "connect to widget input";
+ delete this.outputs[0].widget;
+
+ this.#removeWidgets();
+ }
+ }
+
+ LiteGraph.registerNodeType(
+ "PrimitiveNode",
+ Object.assign(PrimitiveNode, {
+ title: "Primitive",
+ })
+ );
+ PrimitiveNode.category = "utils";
+ },
+});
diff --git a/ComfyUI/web/extensions/logging.js.example b/ComfyUI/web/extensions/logging.js.example
new file mode 100644
index 0000000000000000000000000000000000000000..d015096a29f2732135b827a0efb513c6bf387bcf
--- /dev/null
+++ b/ComfyUI/web/extensions/logging.js.example
@@ -0,0 +1,55 @@
+import { app } from "../scripts/app.js";
+
+const ext = {
+ // Unique name for the extension
+ name: "Example.LoggingExtension",
+ async init(app) {
+ // Any initial setup to run as soon as the page loads
+ console.log("[logging]", "extension init");
+ },
+ async setup(app) {
+ // Any setup to run after the app is created
+ console.log("[logging]", "extension setup");
+ },
+ async addCustomNodeDefs(defs, app) {
+ // Add custom node definitions
+ // These definitions will be configured and registered automatically
+ // defs is a lookup core nodes, add yours into this
+ console.log("[logging]", "add custom node definitions", "current nodes:", Object.keys(defs));
+ },
+ async getCustomWidgets(app) {
+ // Return custom widget types
+ // See ComfyWidgets for widget examples
+ console.log("[logging]", "provide custom widgets");
+ },
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ // Run custom logic before a node definition is registered with the graph
+ console.log("[logging]", "before register node: ", nodeType, nodeData);
+
+ // This fires for every node definition so only log once
+ delete ext.beforeRegisterNodeDef;
+ },
+ async registerCustomNodes(app) {
+ // Register any custom node implementations here allowing for more flexability than a custom node def
+ console.log("[logging]", "register custom nodes");
+ },
+ loadedGraphNode(node, app) {
+ // Fires for each node when loading/dragging/etc a workflow json or png
+ // If you break something in the backend and want to patch workflows in the frontend
+ // This is the place to do this
+ console.log("[logging]", "loaded graph node: ", node);
+
+ // This fires for every node on each load so only log once
+ delete ext.loadedGraphNode;
+ },
+ nodeCreated(node, app) {
+ // Fires every time a node is constructed
+ // You can modify widgets/add handlers/etc here
+ console.log("[logging]", "node created: ", node);
+
+ // This fires for every node so only log once
+ delete ext.nodeCreated;
+ }
+};
+
+app.registerExtension(ext);
diff --git a/ComfyUI/web/extensions/tinyterraNodes/ttN.js b/ComfyUI/web/extensions/tinyterraNodes/ttN.js
new file mode 100644
index 0000000000000000000000000000000000000000..660b8e89878b5f7159233ba68c63e7cd52ff414f
--- /dev/null
+++ b/ComfyUI/web/extensions/tinyterraNodes/ttN.js
@@ -0,0 +1,571 @@
+import { app } from "../../scripts/app.js";
+import { ComfyWidgets } from "../../scripts/widgets.js";
+
+const CONVERTED_TYPE = "converted-widget";
+const GET_CONFIG = Symbol();
+
+function hideWidget(node, widget, suffix = "") {
+ widget.origType = widget.type;
+ widget.origComputeSize = widget.computeSize;
+ widget.origSerializeValue = widget.serializeValue;
+ widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
+ widget.type = CONVERTED_TYPE + suffix;
+ widget.serializeValue = () => {
+ // Prevent serializing the widget if we have no input linked
+ if (!node.inputs) {
+ return undefined;
+ }
+ let node_input = node.inputs.find((i) => i.widget?.name === widget.name);
+
+ if (!node_input || !node_input.link) {
+ return undefined;
+ }
+ return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
+ };
+
+ // Hide any linked widgets, e.g. seed+seedControl
+ if (widget.linkedWidgets) {
+ for (const w of widget.linkedWidgets) {
+ hideWidget(node, w, ":" + widget.name);
+ }
+ }
+}
+
+function convertToInput(node, widget, config) {
+ console.log('config:', config)
+ hideWidget(node, widget);
+
+ const { type } = getWidgetType(config);
+
+ // Add input and store widget config for creating on primitive node
+ const sz = node.size;
+ node.addInput(widget.name, type, {
+ widget: { name: widget.name, [GET_CONFIG]: () => config },
+ });
+
+ for (const widget of node.widgets) {
+ widget.last_y += LiteGraph.NODE_SLOT_HEIGHT;
+ }
+
+ // Restore original size but grow if needed
+ node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
+}
+
+function getWidgetType(config) {
+ // Special handling for COMBO so we restrict links based on the entries
+ let type = config[0];
+ if (type instanceof Array) {
+ type = "COMBO";
+ }
+ return { type };
+}
+
+app.registerExtension({
+ name: "comfy.ttN",
+ init() {
+ const ttNreloadNode = function (node) {
+ const nodeType = node.constructor.type;
+ const origVals = node.properties.origVals || {};
+
+ const nodeTitle = origVals.title || node.title;
+ const nodeColor = origVals.color || node.color;
+ const bgColor = origVals.bgcolor || node.bgcolor;
+ const oldNode = node
+ const options = {
+ 'size': [...node.size],
+ 'color': nodeColor,
+ 'bgcolor': bgColor,
+ 'pos': [...node.pos]
+ }
+
+ let inputLinks = []
+ let outputLinks = []
+ for (const input of node.inputs) {
+ if (input.link) {
+ const input_name = input.name
+ const input_slot = node.findInputSlot(input_name)
+ const input_node = node.getInputNode(input_slot)
+ const input_link = node.getInputLink(input_slot)
+
+ inputLinks.push([input_link.origin_slot, input_node, input_name])
+ }
+ }
+ for (const output of node.outputs) {
+ if (output.links) {
+ const output_name = output.name
+
+ for (const linkID of output.links) {
+ const output_link = graph.links[linkID]
+ const output_node = graph._nodes_by_id[output_link.target_id]
+ outputLinks.push([output_name, output_node, output_link.target_slot])
+ }
+ }
+ }
+
+ app.graph.remove(node)
+ const newNode = app.graph.add(LiteGraph.createNode(nodeType, nodeTitle, options));
+ if (newNode?.constructor?.hasOwnProperty('ttNnodeVersion')) {
+ newNode.properties.ttNnodeVersion = newNode.constructor.ttNnodeVersion;
+ }
+
+ function handleLinks() {
+ // re-convert inputs
+ for (let w of oldNode.widgets) {
+ if (w.type === 'converted-widget') {
+ const WidgetToConvert = newNode.widgets.find((nw) => nw.name === w.name);
+ for (let i of oldNode.inputs) {
+ if (i.name === w.name) {
+ convertToInput(newNode, WidgetToConvert, i.widget);
+ }
+ }
+ }
+ }
+ // replace input and output links
+ for (let input of inputLinks) {
+ const [output_slot, output_node, input_name] = input;
+ output_node.connect(output_slot, newNode.id, input_name)
+ }
+ for (let output of outputLinks) {
+ const [output_name, input_node, input_slot] = output;
+ newNode.connect(output_name, input_node, input_slot)
+ }
+ }
+
+ // fix widget values
+ let values = oldNode.widgets_values;
+ if (!values) {
+ newNode.widgets.forEach((newWidget, index) => {
+ const oldWidget = oldNode.widgets[index];
+ if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) {
+ newWidget.value = oldWidget.value;
+ }
+ });
+ handleLinks();
+ return;
+ }
+ let pass = false
+ const isIterateForwards = values.length <= newNode.widgets.length;
+ let vi = isIterateForwards ? 0 : values.length - 1;
+ function evalWidgetValues(testValue, newWidg) {
+ if (testValue === true || testValue === false) {
+ if (newWidg.options?.on && newWidg.options?.off) {
+ return { value: testValue, pass: true };
+ }
+ } else if (typeof testValue === "number") {
+ if (newWidg.options?.min <= testValue && testValue <= newWidg.options?.max) {
+ return { value: testValue, pass: true };
+ }
+ } else if (newWidg.options?.values?.includes(testValue)) {
+ return { value: testValue, pass: true };
+ } else if (newWidg.inputEl && typeof testValue === "string") {
+ return { value: testValue, pass: true };
+ }
+ return { value: newWidg.value, pass: false };
+ }
+ const updateValue = (wi) => {
+ const oldWidget = oldNode.widgets[wi];
+ let newWidget = newNode.widgets[wi];
+ if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) {
+ while ((isIterateForwards ? vi < values.length : vi >= 0) && !pass) {
+ let { value, pass } = evalWidgetValues(values[vi], newWidget);
+ if (pass && value !== null) {
+ newWidget.value = value;
+ break;
+ }
+ vi += isIterateForwards ? 1 : -1;
+ }
+ vi++
+ if (!isIterateForwards) {
+ vi = values.length - (newNode.widgets.length - 1 - wi);
+ }
+ }
+ };
+ if (isIterateForwards) {
+ for (let wi = 0; wi < newNode.widgets.length; wi++) {
+ updateValue(wi);
+ }
+ } else {
+ for (let wi = newNode.widgets.length - 1; wi >= 0; wi--) {
+ updateValue(wi);
+ }
+ }
+ handleLinks();
+ };
+
+ const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
+ LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
+ const options = getNodeMenuOptions.apply(this, arguments);
+ node.setDirtyCanvas(true, true);
+
+ options.splice(options.length - 1, 0,
+ {
+ content: "Reload Node (ttN)",
+ callback: () => {
+ var graphcanvas = LGraphCanvas.active_canvas;
+ if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
+ ttNreloadNode(node);
+ } else {
+ for (var i in graphcanvas.selected_nodes) {
+ ttNreloadNode(graphcanvas.selected_nodes[i]);
+ }
+ }
+ }
+ },
+ );
+ return options;
+ };
+ },
+ beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name.startsWith("ttN")) {
+ const origOnConfigure = nodeType.prototype.onConfigure;
+ nodeType.prototype.onConfigure = function () {
+ const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined;
+ let nodeVersion = nodeData.input.hidden?.ttNnodeVersion ? nodeData.input.hidden.ttNnodeVersion : null;
+ nodeType.ttNnodeVersion = nodeVersion;
+ this.properties['ttNnodeVersion'] = this.properties['ttNnodeVersion'] ? this.properties['ttNnodeVersion'] : nodeVersion;
+ if (this.properties['ttNnodeVersion'] !== nodeVersion) {
+ if (!this.properties['origVals']) {
+ this.properties['origVals'] = { bgcolor: this.bgcolor, color: this.color, title: this.title }
+ }
+ this.bgcolor = "#d82129";
+ this.color = "#bd000f";
+ this.title = this.title.includes("Node Version Mismatch") ? this.title : this.title + " - Node Version Mismatch"
+ } else if (this.properties['origVals']) {
+ this.bgcolor = this.properties.origVals.bgcolor;
+ this.color = this.properties.origVals.color;
+ this.title = this.properties.origVals.title;
+ delete this.properties['origVals']
+ }
+ return r;
+ };
+ }
+ if (nodeData.name === "ttN textDebug") {
+ const onNodeCreated = nodeType.prototype.onNodeCreated;
+ nodeType.prototype.onNodeCreated = function () {
+ const r = onNodeCreated?.apply(this, arguments);
+ const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
+ w.inputEl.readOnly = true;
+ w.inputEl.style.opacity = 0.7;
+ return r;
+ };
+
+ const onExecuted = nodeType.prototype.onExecuted;
+ nodeType.prototype.onExecuted = function (message) {
+ onExecuted?.apply(this, arguments);
+
+ for (const widget of this.widgets) {
+ if (widget.type === "customtext"){
+ widget.value = message.text.join('');
+ }
+ }
+
+ this.onResize?.(this.size);
+ };
+ }
+ },
+ nodeCreated(node) {
+ if (node.getTitle() === "pipeLoader") {
+ for (let widget of node.widgets) {
+ if (widget.name === "control_after_generate") {
+ widget.value = "fixed"
+ }
+ }
+ }
+ },
+});
+
+
+// ttN Dropdown
+var styleElement = document.createElement("style");
+const cssCode = `
+.ttN-dropdown, .ttN-nested-dropdown {
+ position: relative;
+ box-sizing: border-box;
+ background-color: #171717;
+ box-shadow: 0 4px 4px rgba(255, 255, 255, .25);
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ z-index: 1000;
+ overflow: visible;
+ max-height: fit-content;
+ max-width: fit-content;
+}
+
+.ttN-dropdown {
+ position: absolute;
+ border-radius: 0;
+}
+
+/* Style for final items */
+.ttN-dropdown li.item, .ttN-nested-dropdown li.item {
+ font-weight: normal;
+ min-width: max-content;
+}
+
+/* Style for folders (parent items) */
+.ttN-dropdown li.folder, .ttN-nested-dropdown li.folder {
+ cursor: default;
+ position: relative;
+ border-right: 3px solid cyan;
+}
+
+.ttN-dropdown li.folder::after, .ttN-nested-dropdown li.folder::after {
+ content: ">";
+ position: absolute;
+ right: 2px;
+ font-weight: normal;
+}
+
+.ttN-dropdown li, .ttN-nested-dropdown li {
+ padding: 4px 10px;
+ cursor: pointer;
+ font-family: system-ui;
+ font-size: 0.7rem;
+ position: relative;
+}
+
+/* Style for nested dropdowns */
+.ttN-nested-dropdown {
+ position: absolute;
+ top: 0;
+ left: 100%;
+ margin: 0;
+ border: none;
+ display: none;
+}
+
+.ttN-dropdown li.selected > .ttN-nested-dropdown,
+.ttN-nested-dropdown li.selected > .ttN-nested-dropdown {
+ display: block;
+ border: none;
+}
+
+.ttN-dropdown li.selected,
+.ttN-nested-dropdown li.selected {
+ background-color: #e5e5e5;
+ border: none;
+}
+`
+styleElement.innerHTML = cssCode
+document.head.appendChild(styleElement);
+
+let activeDropdown = null;
+
+export function ttN_RemoveDropdown() {
+ if (activeDropdown) {
+ activeDropdown.removeEventListeners();
+ activeDropdown.dropdown.remove();
+ activeDropdown = null;
+ }
+}
+
+class Dropdown {
+ constructor(inputEl, suggestions, onSelect, isDict = false) {
+ this.dropdown = document.createElement('ul');
+ this.dropdown.setAttribute('role', 'listbox');
+ this.dropdown.classList.add('ttN-dropdown');
+ this.selectedIndex = -1;
+ this.inputEl = inputEl;
+ this.suggestions = suggestions;
+ this.onSelect = onSelect;
+ this.isDict = isDict;
+
+ this.focusedDropdown = this.dropdown;
+
+ this.buildDropdown();
+
+ this.onKeyDownBound = this.onKeyDown.bind(this);
+ this.onWheelBound = this.onWheel.bind(this);
+ this.onClickBound = this.onClick.bind(this);
+
+ this.addEventListeners();
+ }
+
+ buildDropdown() {
+ if (this.isDict) {
+ this.buildNestedDropdown(this.suggestions, this.dropdown);
+ } else {
+ this.suggestions.forEach((suggestion, index) => {
+ this.addListItem(suggestion, index, this.dropdown);
+ });
+ }
+
+ const inputRect = this.inputEl.getBoundingClientRect();
+ this.dropdown.style.top = (inputRect.top + inputRect.height - 10) + 'px';
+ this.dropdown.style.left = inputRect.left + 'px';
+
+ document.body.appendChild(this.dropdown);
+ activeDropdown = this;
+ }
+
+ buildNestedDropdown(dictionary, parentElement) {
+ let index = 0;
+ Object.keys(dictionary).forEach((key) => {
+ const item = dictionary[key];
+ if (typeof item === "object" && item !== null) {
+ const nestedDropdown = document.createElement('ul');
+ nestedDropdown.setAttribute('role', 'listbox');
+ nestedDropdown.classList.add('ttN-nested-dropdown');
+ const parentListItem = document.createElement('li');
+ parentListItem.classList.add('folder');
+ parentListItem.textContent = key;
+ parentListItem.appendChild(nestedDropdown);
+ parentListItem.addEventListener('mouseover', this.onMouseOver.bind(this, index, parentElement));
+ parentElement.appendChild(parentListItem);
+ this.buildNestedDropdown(item, nestedDropdown);
+ index = index + 1;
+ } else {
+ const listItem = document.createElement('li');
+ listItem.classList.add('item');
+ listItem.setAttribute('role', 'option');
+ listItem.textContent = key;
+ listItem.addEventListener('mouseover', this.onMouseOver.bind(this, index, parentElement));
+ listItem.addEventListener('mousedown', this.onMouseDown.bind(this, key));
+ parentElement.appendChild(listItem);
+ index = index + 1;
+ }
+ });
+ }
+
+ addListItem(item, index, parentElement) {
+ const listItem = document.createElement('li');
+ listItem.setAttribute('role', 'option');
+ listItem.textContent = item;
+ listItem.addEventListener('mouseover', this.onMouseOver.bind(this, index));
+ listItem.addEventListener('mousedown', this.onMouseDown.bind(this, item));
+ parentElement.appendChild(listItem);
+ }
+
+ addEventListeners() {
+ document.addEventListener('keydown', this.onKeyDownBound);
+ this.dropdown.addEventListener('wheel', this.onWheelBound);
+ document.addEventListener('click', this.onClickBound);
+ }
+
+ removeEventListeners() {
+ document.removeEventListener('keydown', this.onKeyDownBound);
+ this.dropdown.removeEventListener('wheel', this.onWheelBound);
+ document.removeEventListener('click', this.onClickBound);
+ }
+
+ onMouseOver(index, parentElement) {
+ if (parentElement) {
+ this.focusedDropdown = parentElement;
+ }
+ this.selectedIndex = index;
+ this.updateSelection();
+ }
+
+ onMouseOut() {
+ this.selectedIndex = -1;
+ this.updateSelection();
+ }
+
+ onMouseDown(suggestion, event) {
+ event.preventDefault();
+ this.onSelect(suggestion);
+ this.dropdown.remove();
+ this.removeEventListeners();
+ }
+
+ onKeyDown(event) {
+ const enterKeyCode = 13;
+ const escKeyCode = 27;
+ const arrowUpKeyCode = 38;
+ const arrowDownKeyCode = 40;
+ const arrowRightKeyCode = 39;
+ const arrowLeftKeyCode = 37;
+ const tabKeyCode = 9;
+
+ const items = Array.from(this.focusedDropdown.children);
+ const selectedItem = items[this.selectedIndex];
+
+ if (activeDropdown) {
+ if (event.keyCode === arrowUpKeyCode) {
+ event.preventDefault();
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
+ this.updateSelection();
+ }
+
+ else if (event.keyCode === arrowDownKeyCode) {
+ event.preventDefault();
+ this.selectedIndex = Math.min(items.length - 1, this.selectedIndex + 1);
+ this.updateSelection();
+ }
+
+ else if (event.keyCode === arrowRightKeyCode) {
+ event.preventDefault();
+ if (selectedItem && selectedItem.classList.contains('folder')) {
+ const nestedDropdown = selectedItem.querySelector('.ttN-nested-dropdown');
+ if (nestedDropdown) {
+ this.focusedDropdown = nestedDropdown;
+ this.selectedIndex = 0;
+ this.updateSelection();
+ }
+ }
+ }
+
+ else if (event.keyCode === arrowLeftKeyCode && this.focusedDropdown !== this.dropdown) {
+ const parentDropdown = this.focusedDropdown.closest('.ttN-dropdown, .ttN-nested-dropdown').parentNode.closest('.ttN-dropdown, .ttN-nested-dropdown');
+ if (parentDropdown) {
+ this.focusedDropdown = parentDropdown;
+ this.selectedIndex = Array.from(parentDropdown.children).indexOf(this.focusedDropdown.parentNode);
+ this.updateSelection();
+ }
+ }
+
+ else if ((event.keyCode === enterKeyCode || event.keyCode === tabKeyCode) && this.selectedIndex >= 0) {
+ event.preventDefault();
+ if (selectedItem.classList.contains('item')) {
+ this.onSelect(items[this.selectedIndex].textContent);
+ this.dropdown.remove();
+ this.removeEventListeners();
+ }
+
+ const nestedDropdown = selectedItem.querySelector('.ttN-nested-dropdown');
+ if (nestedDropdown) {
+ this.focusedDropdown = nestedDropdown;
+ this.selectedIndex = 0;
+ this.updateSelection();
+ }
+ }
+
+ else if (event.keyCode === escKeyCode) {
+ this.dropdown.remove();
+ this.removeEventListeners();
+ }
+ }
+ }
+
+ onWheel(event) {
+ const top = parseInt(this.dropdown.style.top);
+ if (localStorage.getItem("Comfy.Settings.Comfy.InvertMenuScrolling")) {
+ this.dropdown.style.top = (top + (event.deltaY < 0 ? 10 : -10)) + "px";
+ } else {
+ this.dropdown.style.top = (top + (event.deltaY < 0 ? -10 : 10)) + "px";
+ }
+ }
+
+ onClick(event) {
+ if (!this.dropdown.contains(event.target) && event.target !== this.inputEl) {
+ this.dropdown.remove();
+ this.removeEventListeners();
+ }
+ }
+
+ updateSelection() {
+ Array.from(this.focusedDropdown.children).forEach((li, index) => {
+ if (index === this.selectedIndex) {
+ li.classList.add('selected');
+ } else {
+ li.classList.remove('selected');
+ }
+ });
+ }
+}
+
+export function ttN_CreateDropdown(inputEl, suggestions, onSelect, isDict = false) {
+ ttN_RemoveDropdown();
+ new Dropdown(inputEl, suggestions, onSelect, isDict);
+}
\ No newline at end of file
diff --git a/ComfyUI/web/extensions/tinyterraNodes/ttNdynamicWidgets.js b/ComfyUI/web/extensions/tinyterraNodes/ttNdynamicWidgets.js
new file mode 100644
index 0000000000000000000000000000000000000000..08412f5599b8b59499030f906090be926c081785
--- /dev/null
+++ b/ComfyUI/web/extensions/tinyterraNodes/ttNdynamicWidgets.js
@@ -0,0 +1,304 @@
+import { app } from "../../scripts/app.js";
+
+let origProps = {};
+
+const findWidgetByName = (node, name) => node.widgets.find((w) => w.name === name);
+
+const doesInputWithNameExist = (node, name) => node.inputs ? node.inputs.some((input) => input.name === name) : false;
+
+function updateNodeHeight(node) {
+ node.setSize([node.size[0], node.computeSize()[1]]);
+}
+
+function toggleWidget(node, widget, show = false, suffix = "") {
+ if (!widget || doesInputWithNameExist(node, widget.name)) return;
+ if (!origProps[widget.name]) {
+ origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize };
+ }
+ const origSize = node.size;
+
+ widget.type = show ? origProps[widget.name].origType : "ttNhidden" + suffix;
+ widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4];
+
+ widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show));
+
+ const height = show ? Math.max(node.computeSize()[1], origSize[1]) : node.size[1];
+ node.setSize([node.size[0], height]);
+
+}
+
+function widgetLogic(node, widget) {
+ if (widget.name === 'lora_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'lora_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'lora1_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'lora1_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora1_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora1_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora1_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'lora2_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'lora2_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora2_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora2_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora2_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'lora3_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'lora3_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora3_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora3_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora3_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'refiner_ckpt_name') {
+ let refiner_lora1 = findWidgetByName(node, 'refiner_lora1_name').value
+ let refiner_lora2 = findWidgetByName(node, 'refiner_lora2_name').value
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'refiner_vae_name'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_name'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_clip_strength'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_name'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'refiner_vae_name'), true)
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_name'), true)
+ if (refiner_lora1 !== "None") {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_clip_strength'), true)
+ }
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_name'), true)
+ if (refiner_lora2 !== "None") {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_clip_strength'), true)
+ }
+ }
+ }
+ if (widget.name === 'refiner_lora1_name') {
+ let refiner_ckpt = findWidgetByName(node, 'refiner_ckpt_name').value
+
+ if (widget.value === "None" || refiner_ckpt === "None") {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora1_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'refiner_lora2_name') {
+ let refiner_ckpt = findWidgetByName(node, 'refiner_ckpt_name').value
+
+ if (widget.value === "None" || refiner_ckpt === "None") {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'refiner_lora2_clip_strength'), true)
+ }
+ }
+ if (widget.name === 'rescale_after_model') {
+ if (widget.value === false) {
+ toggleWidget(node, findWidgetByName(node, 'rescale_method'))
+ toggleWidget(node, findWidgetByName(node, 'rescale'))
+ toggleWidget(node, findWidgetByName(node, 'percent'))
+ toggleWidget(node, findWidgetByName(node, 'width'))
+ toggleWidget(node, findWidgetByName(node, 'height'))
+ toggleWidget(node, findWidgetByName(node, 'longer_side'))
+ toggleWidget(node, findWidgetByName(node, 'crop'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'rescale_method'), true)
+ toggleWidget(node, findWidgetByName(node, 'rescale'), true)
+
+ let rescale_value = findWidgetByName(node, 'rescale').value
+
+ if (rescale_value === 'by percentage') {
+ toggleWidget(node, findWidgetByName(node, 'percent'), true)
+ } else if (rescale_value === 'to Width/Height') {
+ toggleWidget(node, findWidgetByName(node, 'width'), true)
+ toggleWidget(node, findWidgetByName(node, 'height'), true)
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'longer_side'), true)
+ }
+ toggleWidget(node, findWidgetByName(node, 'crop'), true)
+ }
+ }
+ if (widget.name === 'rescale') {
+ let rescale_after_model = findWidgetByName(node, 'rescale_after_model').value
+ if (widget.value === 'by percentage' && rescale_after_model) {
+ toggleWidget(node, findWidgetByName(node, 'width'))
+ toggleWidget(node, findWidgetByName(node, 'height'))
+ toggleWidget(node, findWidgetByName(node, 'longer_side'))
+ toggleWidget(node, findWidgetByName(node, 'percent'), true)
+ } else if (widget.value === 'to Width/Height' && rescale_after_model) {
+ toggleWidget(node, findWidgetByName(node, 'width'), true)
+ toggleWidget(node, findWidgetByName(node, 'height'), true)
+ toggleWidget(node, findWidgetByName(node, 'percent'))
+ toggleWidget(node, findWidgetByName(node, 'longer_side'))
+ } else if (rescale_after_model) {
+ toggleWidget(node, findWidgetByName(node, 'longer_side'), true)
+ toggleWidget(node, findWidgetByName(node, 'width'))
+ toggleWidget(node, findWidgetByName(node, 'height'))
+ toggleWidget(node, findWidgetByName(node, 'percent'))
+ }
+ }
+ if (widget.name === 'upscale_method') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'factor'))
+ toggleWidget(node, findWidgetByName(node, 'crop'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'factor'), true)
+ toggleWidget(node, findWidgetByName(node, 'crop'), true)
+ }
+ }
+ if (widget.name === 'image_output') {
+ if (widget.value === 'Hide' || widget.value === 'Preview') {
+ toggleWidget(node, findWidgetByName(node, 'save_prefix'))
+ toggleWidget(node, findWidgetByName(node, 'output_path'))
+ toggleWidget(node, findWidgetByName(node, 'embed_workflow'))
+ toggleWidget(node, findWidgetByName(node, 'number_padding'))
+ toggleWidget(node, findWidgetByName(node, 'overwrite_existing'))
+ } else if (widget.value === 'Save' || widget.value === 'Hide/Save') {
+ toggleWidget(node, findWidgetByName(node, 'save_prefix'), true)
+ toggleWidget(node, findWidgetByName(node, 'output_path'), true)
+ toggleWidget(node, findWidgetByName(node, 'embed_workflow'), true)
+ toggleWidget(node, findWidgetByName(node, 'number_padding'), true)
+ toggleWidget(node, findWidgetByName(node, 'overwrite_existing'), true)
+ }
+ }
+ if (widget.name === 'add_noise') {
+ if (widget.value === "disable") {
+ toggleWidget(node, findWidgetByName(node, 'noise_seed'))
+ toggleWidget(node, findWidgetByName(node, 'control_after_generate'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'noise_seed'), true)
+ toggleWidget(node, findWidgetByName(node, 'control_after_generate'), true)
+ }
+ }
+ if (widget.name === 'ckpt_B_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'config_B_name'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'config_B_name'), true)
+ }
+ }
+ if (widget.name === 'ckpt_C_name') {
+ if (widget.value === "None") {
+ toggleWidget(node, findWidgetByName(node, 'config_C_name'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'config_C_name'), true)
+ }
+ }
+ if (widget.name === 'save_model') {
+ if (widget.value === "True") {
+ toggleWidget(node, findWidgetByName(node, 'save_prefix'), true)
+
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'save_prefix'))
+ }
+ }
+ if (widget.name === 'num_loras') {
+ let number_to_show = widget.value + 1
+ for (let i = 0; i < number_to_show; i++) {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_name'), true)
+ if (findWidgetByName(node, 'mode').value === "simple") {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'), true)
+ }
+ }
+ for (let i = number_to_show; i < 21; i++) {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_name'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'))
+ }
+ updateNodeHeight(node)
+ }
+ if (widget.name === 'mode') {
+ let number_to_show = findWidgetByName(node, 'num_loras').value + 1
+ for (let i = 0; i < number_to_show; i++) {
+ if (widget.value === "simple") {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'))
+ } else {
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'))
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'), true)
+ toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'), true)}
+ }
+ updateNodeHeight(node)
+ }
+ if (widget.name === 'toggle') {
+ widget.type = 'toggle'
+ widget.options = {on: 'Enabled', off: 'Disabled'}
+ }
+}
+
+const getSetWidgets = ['rescale_after_model', 'rescale', 'image_output',
+ 'lora_name', 'lora1_name', 'lora2_name', 'lora3_name',
+ 'refiner_lora1_name', 'refiner_lora2_name', 'upscale_method',
+ 'image_output', 'add_noise',
+ 'ckpt_B_name', 'ckpt_C_name', 'save_model', 'refiner_ckpt_name',
+ 'num_loras', 'mode', 'toggle']
+
+function getSetters(node) {
+ if (node.widgets)
+ for (const w of node.widgets) {
+ if (getSetWidgets.includes(w.name)) {
+ widgetLogic(node, w);
+ let widgetValue = w.value;
+
+ // Define getters and setters for widget values
+ Object.defineProperty(w, 'value', {
+ get() {
+ return widgetValue;
+ },
+ set(newVal) {
+ if (newVal !== widgetValue) {
+ widgetValue = newVal;
+ widgetLogic(node, w);
+ }
+ }
+ });
+ }
+ }
+}
+
+app.registerExtension({
+ name: "comfy.ttN.dynamicWidgets",
+
+ nodeCreated(node) {
+ if (node.getTitle() == "hiresfixScale" ||
+ node.getTitle() == "pipeLoader" ||
+ node.getTitle() == "pipeLoaderSDXL" ||
+ node.getTitle() == "pipeKSampler" ||
+ node.getTitle() == "pipeKSamplerAdvanced" ||
+ node.getTitle() == "pipeKSamplerSDXL" ||
+ node.getTitle() == "imageRemBG" ||
+ node.getTitle() == "imageOutput"||
+ node.getTitle() == "multiModelMerge" ||
+ node.getTitle() == "pipeLoraStack" ||
+ node.getTitle() == "pipeEncodeConcat") {
+ getSetters(node)
+ }
+ }
+});
diff --git a/ComfyUI/web/extensions/tinyterraNodes/ttNembedAC.js b/ComfyUI/web/extensions/tinyterraNodes/ttNembedAC.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e673c0c4e98e0e6b84f7b58dca13729b3892d31
--- /dev/null
+++ b/ComfyUI/web/extensions/tinyterraNodes/ttNembedAC.js
@@ -0,0 +1,170 @@
+// Imports specific objects from other modules.
+import { app } from "../../scripts/app.js";
+import { ttN_CreateDropdown, ttN_RemoveDropdown } from "./ttN.js";
+
+// Initialize some global lists and objects.
+let embeddingsList = [];
+let embeddingFiles = [];
+let embeddingsHierarchy = {};
+
+// Convert a list of strings into a hierarchical structure.
+function convertListToHierarchy(list) {
+ const hierarchy = {}; // Initialize an empty hierarchy object.
+
+ // Iterate over each item in the list.
+ for (var item of list) {
+ item = item.replace("embedding:", ""); // Remove any "embedding:" prefix from the item.
+ const parts = item.split(/:\\|\\/); // Split the item by either ':\' or '\'.
+ let currentNode = hierarchy; // Start at the root of the hierarchy.
+
+ // For each part of the split item...
+ parts.forEach((part, index) => {
+ // If it's the last part, set its value to null in the hierarchy.
+ if (index === parts.length - 1) {
+ currentNode[part] = null;
+ } else {
+ // Otherwise, initialize the node if it doesn't exist yet and move deeper into the hierarchy.
+ currentNode[part] = currentNode[part] || {};
+ currentNode = currentNode[part];
+ }
+ });
+ }
+
+ return hierarchy; // Return the filled hierarchy.
+}
+
+// Register an extension to the app.
+app.registerExtension({
+ name: "comfy.ttN.embeddingAC",
+ // Before a node definition is registered...
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ // If the node name matches a specific type...
+ if (nodeData.name === "ttN pipeKSampler") {
+ initializeEmbeddingData(nodeData.input.hidden.embeddingsList[0]);
+ }
+ },
+ // When a node is created...
+ nodeCreated(node) {
+ // If the node has widgets and its title isn't "xyPlot"...
+ if (node.widgets && node.getTitle() !== "xyPlot") {
+ const relevantWidgets = filterRelevantWidgets(node.widgets); // Filter out the relevant widgets.
+ addInputListenersToWidgets(relevantWidgets); // Add input listeners to these widgets.
+ }
+ }
+});
+
+// Returns a list of widgets that either have type "customtext" with dynamic prompts or just have dynamic prompts.
+function filterRelevantWidgets(widgets) {
+ return widgets.filter(widget => (widget.type === "customtext" && widget.dynamicPrompts !== false) || widget.dynamicPrompts);
+}
+
+// Adds input listeners to the given widgets.
+function addInputListenersToWidgets(widgets) {
+ widgets.forEach(widget => {
+ const inputHandler = createWidgetInputHandler(widget); // Create an input handler specific for this widget.
+ setWidgetInputHandler(widget, inputHandler); // Set this handler to the widget.
+ });
+}
+
+// Returns a function that will handle the widget's input.
+function createWidgetInputHandler(widget) {
+ return function handleInput() {
+ const currentWord = getCurrentWordFromInput(widget); // Get the word at the current cursor position in the widget's input.
+ // Check if the current word should trigger embedding suggestions...
+ if (shouldProvideEmbeddingSuggestion(currentWord)) {
+ const suggestions = filterEmbeddingsForInput(currentWord); // Get suggestions for the current word.
+ if (suggestions.length > 0) { // If there are suggestions...
+ // Convert the suggestions to a hierarchy and create a dropdown with these suggestions.
+ embeddingsHierarchy = convertListToHierarchy(suggestions);
+ ttN_CreateDropdown(widget.inputEl, embeddingsHierarchy, selectedSuggestion => {
+ // Update the widget's input value with the selected suggestion when one is chosen.
+ widget.inputEl.value = updateInputWithSuggestion(widget.inputEl.value, selectedSuggestion, widget);
+ }, true);
+ return;
+ }
+ }
+ // If no suggestions, remove any existing dropdown.
+ ttN_RemoveDropdown();
+ };
+}
+
+// Adds or replaces event listeners for the widget's input.
+function setWidgetInputHandler(widget, handler) {
+ ['input', 'mousedown'].forEach(event => {
+ // Remove any existing listeners and then add the new handler.
+ widget.inputEl.removeEventListener(event, handler);
+ widget.inputEl.addEventListener(event, handler);
+ });
+}
+
+// Returns the word at the current cursor position from the widget's input.
+function getCurrentWordFromInput(widget) {
+ const cursorPosition = widget.inputEl.selectionStart;
+ const segments = widget.inputEl.value.split(' ');
+ return segments[widget.inputEl.value.substring(0, cursorPosition).split(' ').length - 1].toLowerCase();
+}
+
+// Determines if the current word should trigger embedding suggestions.
+function shouldProvideEmbeddingSuggestion(word) {
+ const suggestionPrefix = 'embedding:';
+ return suggestionPrefix.startsWith(word) && word.length > 2 || word.startsWith(suggestionPrefix);
+}
+
+// Filters embeddings based on a specific word.
+function filterEmbeddingsForInput(input) {
+ const prefixes = ['embedding', 'embeddin', 'embeddi', 'embedd', 'embed', 'embe', 'emb']
+
+ let inputLowered = input.toLowerCase();
+ let cleanedInput = inputLowered.replace('embedding:', '');
+
+ prefixes.forEach(prefix => {
+ if (inputLowered.startsWith(prefix)) {
+ cleanedInput = cleanedInput.replace(prefix, '');
+ }
+ })
+
+ cleanedInput = cleanedInput.replace(/\//g, "\\");
+
+ return embeddingsList.filter(embedding => {
+ const embeddingName = getFileName(embedding).toLowerCase();
+ embedding = embedding.replace('embedding:', '').toLowerCase();
+ if (embeddingName.startsWith(cleanedInput) || embedding.startsWith(cleanedInput) || prefixes.includes(cleanedInput)) {
+ return true;
+ }
+ return false
+ });
+}
+
+function getFileName(path) {
+ const parts = path.split(/[\/:\\]/); // Split the path by '/' or ':'
+ const fileName = parts[parts.length - 1]; // Get the last part (filename with extension)
+ return fileName;
+}
+
+// Updates the widget's input text with a selected suggestion.
+function updateInputWithSuggestion(inputText, selectedSuggestion, widget) {
+ const cursorPosition = widget.inputEl.selectionStart;
+ const inputSegments = inputText.split(' ');
+ const cursorSegmentIndex = inputText.substring(0, cursorPosition).split(' ').length - 1;
+
+ if (inputSegments[cursorSegmentIndex].startsWith('emb')) {
+ inputSegments[cursorSegmentIndex] = 'embedding:' + selectedSuggestion;
+ }
+
+ return inputSegments.join(' ');
+}
+
+// Initializes data related to embeddings.
+function initializeEmbeddingData(initialEmbeddingsList) {
+ embeddingsList = initialEmbeddingsList;
+
+ embeddingsList.forEach(embedding => {
+ const fileName = embedding.split('\\').slice(-1)[0];
+ embeddingFiles.push(fileName);
+ });
+
+ embeddingsList = embeddingsList.map(embedding => {
+ const segments = embedding.split('/');
+ return segments.map((segment, index) => "embedding:" + segments.slice(0, index + 1).join('/'));
+ }).flat();
+}
diff --git a/ComfyUI/web/extensions/tinyterraNodes/ttNfullscreen.js b/ComfyUI/web/extensions/tinyterraNodes/ttNfullscreen.js
new file mode 100644
index 0000000000000000000000000000000000000000..1333e605b222e2eac91a448eb2074ffa3714c46d
--- /dev/null
+++ b/ComfyUI/web/extensions/tinyterraNodes/ttNfullscreen.js
@@ -0,0 +1,540 @@
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js";
+
+const FULLSCREEN_WRAPPER_ID = "ttN-FullscreenWrapper";
+const FULLSCREEN_IMAGE_ID = "ttN-FullscreenImage";
+const IMAGE_PREVIEWS_WRAPPER_ID = "ttN-imagePreviewsWrapper";
+
+let ttN_isFullscreen = false;
+let ttN_FullscreenImage = new Image();
+let ttN_FullscreenImageIndex = 0;
+let ttN_FullscreenNode = null;
+let ttN_Slideshow = true;
+
+let ttN_srcDict = {};
+let ttN_imageElementsDict = {};
+
+loadSrcDict()
+
+const ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
+
+function saveSrcDict() {
+ sessionStorage.setItem('ttN_srcDict', JSON.stringify(ttN_srcDict));
+}
+
+function loadSrcDict() {
+ const savedData = sessionStorage.getItem('ttN_srcDict');
+ if (savedData) {
+ ttN_srcDict = JSON.parse(savedData);
+ }
+}
+
+function clearSrcDict() {
+ ttN_srcDict = {};
+ sessionStorage.removeItem('ttN_srcDict');
+}
+
+function _getSelectedNode() {
+ const graphcanvas = LGraphCanvas.active_canvas;
+ if (graphcanvas.selected_nodes && Object.keys(graphcanvas.selected_nodes).length === 1) {
+ return Object.values(graphcanvas.selected_nodes)[0];
+ }
+ return null;
+}
+
+function _findFullImageSRC(node) {
+ if (node.imgs) {
+ let img = node.imgs.find(imgElement => imgElement.src.includes("filename"));
+ return img ? img.src : null;
+ }
+ return null;
+}
+
+function _findLatentPreviewImageSRC(node) {
+ if (!node.imgs) return null;
+
+ if (node.imageIndex !== null && node.imageIndex < node.imgs.length) {
+ return node.imgs[node.imageIndex].src;
+ } else if (node.overIndex !== null && node.overIndex < node.imgs.length) {
+ return node.imgs[node.overIndex].src;
+ }
+ return null;
+}
+
+function _initiateFullscreen(Element) {
+ if (Element.requestFullscreen) {
+ return Element.requestFullscreen();
+ } else if (Element.mozRequestFullScreen) {
+ return Element.mozRequestFullScreen();
+ } else if (Element.webkitRequestFullscreen) {
+ return Element.webkitRequestFullscreen();
+ } else if (Element.msRequestFullscreen) {
+ return Element.msRequestFullscreen();
+ }
+}
+
+function _applyTranslation(Element, Index, List) {
+ let translationConst = (Index / (List.length - 1)) * 100;
+
+ translationConst = translationConst - (0.5 / (List.length - 1)) * 100
+
+ if (Index === 0) { translationConst = 0; }
+ Element.style.transform = 'translateX(-' + translationConst + '%)';
+}
+
+function _handleArrowKeys(e) {
+ e.stopPropagation();
+
+ const FullscreenWrapper = document.getElementById(FULLSCREEN_WRAPPER_ID);
+ const imagePreviewsWrapper = document.getElementById(IMAGE_PREVIEWS_WRAPPER_ID);
+ const imageList = ttN_srcDict[ttN_FullscreenNode.id] || [];
+ const comfyMenu = document.getElementsByClassName("comfy-menu")[0]
+ const litegraph = document.getElementsByClassName("litegraph")[0]
+
+
+ switch (e.code) {
+ case 'ArrowLeft':
+ if (e.ctrlKey) {
+ ttN_FullscreenImageIndex = 0;
+ break;
+ }
+
+ if (e.shiftKey) {
+ ttN_FullscreenImageIndex -= 5;
+ if (ttN_FullscreenImageIndex < 0) ttN_FullscreenImageIndex = 0
+ break;
+ }
+
+ ttN_FullscreenImageIndex -= 1;
+ if (ttN_FullscreenImageIndex < 0) ttN_FullscreenImageIndex = 0
+ break;
+
+ case 'ArrowRight':
+ if (e.ctrlKey) {
+ ttN_FullscreenImageIndex = imageList.length - 1;
+ break;
+ }
+
+ if (e.shiftKey) {
+ ttN_FullscreenImageIndex += 5;
+ if (ttN_FullscreenImageIndex > imageList.length - 1) ttN_FullscreenImageIndex = imageList.length - 1
+ break;
+ }
+
+ ttN_FullscreenImageIndex += 1;
+ if (ttN_FullscreenImageIndex > imageList.length - 1) ttN_FullscreenImageIndex = imageList.length - 1
+ break;
+
+ case 'ArrowUp':
+ if (imagePreviewsWrapper) {
+ if (imagePreviewsWrapper.style.display === 'none') {
+ FullscreenWrapper.append(comfyMenu)
+ imagePreviewsWrapper.style.display = 'flex'
+ } else {
+ litegraph.append(comfyMenu)
+ imagePreviewsWrapper.style.display = 'none'
+ }
+ }
+ break;
+
+ case 'ArrowDown':
+ ttN_Slideshow = !ttN_Slideshow;
+
+ if (ttN_Slideshow) {
+ ttN_FullscreenImageIndex = -1;
+ imagePreviewsWrapper.style.display = 'none'
+ litegraph.append(comfyMenu)
+ } else {
+ imagePreviewsWrapper.style.display = 'flex'
+ FullscreenWrapper.append(comfyMenu)
+ }
+ break;
+ }
+ _applyTranslation(imagePreviewsWrapper, ttN_FullscreenImageIndex, imageList);
+ updateImageElements()
+}
+
+function _handleWheelEvent(e) {
+ e.stopPropagation();
+
+ var InvertMenuScrolling = localStorage.getItem('Comfy.Settings.Comfy.InvertMenuScrolling')
+ console.log("InvertMenuScrolling", InvertMenuScrolling)
+
+ if (InvertMenuScrolling === "true") {
+ console.log('yes')
+ if (e.deltaY > 0) {
+ // Scrolling down
+ ttN_FullscreenImageIndex += 1;
+ } else if (e.deltaY < 0) {
+ // Scrolling up
+ ttN_FullscreenImageIndex -= 1;
+ if (ttN_FullscreenImageIndex < 0) ttN_FullscreenImageIndex = 0;
+ }
+ } else {
+ console.log('no')
+ if (e.deltaY < 0) {
+ // Scrolling down
+ ttN_FullscreenImageIndex += 1;
+ } else if (e.deltaY > 0) {
+ // Scrolling up
+ ttN_FullscreenImageIndex -= 1;
+ if (ttN_FullscreenImageIndex < 0) ttN_FullscreenImageIndex = 0;
+ }
+ }
+ updateImageElements()
+}
+
+function _handleEscapeKey(e) {
+ e.stopPropagation();
+ LGraphCanvas.prototype.ttNcloseFullscreen();
+}
+
+function _handleExecutedEvent(event) {
+ setTimeout(updateImageTLDE, 500);
+}
+
+function _handleReconnectingEvent(e) {
+ clearSrcDict();
+ sessionStorage.removeItem('Comfy.Settings.ttN.default_fullscreen_node');
+ ttN_imageElementsDict = {};
+}
+
+function _triggerFullscreen(node) {
+ updateImageTLDE();
+ ttN_FullscreenImageIndex = -1;
+ LGraphCanvas.prototype.ttNcreateFullscreen(node);
+ ttN_FullscreenNode = node;
+}
+
+function ttNfullscreenEventListener(e) {
+ if (!ttN_isFullscreen) {
+ if ((e.code === 'ArrowUp' && e.shiftKey) && !e.ctrlKey) {
+ e.stopPropagation();
+
+ let selected_node = _getSelectedNode();
+ if (selected_node) {
+ _triggerFullscreen(selected_node);
+ return
+ }
+
+ let defaultNodeID = JSON.parse(sessionStorage.getItem('Comfy.Settings.ttN.default_fullscreen_node'));
+ if (defaultNodeID) {
+ let defaultNode = app.graph._nodes_by_id[defaultNodeID];
+ if (defaultNode) {
+ _triggerFullscreen(defaultNode);
+ return
+ }
+ }
+ }
+
+ return;
+ }
+
+ e.stopPropagation();
+
+ updateImageTLDE();
+
+ const imagePreviewsWrapper = document.getElementById(IMAGE_PREVIEWS_WRAPPER_ID);
+ const imageList = ttN_srcDict[ttN_FullscreenNode.id] || [];
+
+ switch (e.type) {
+ case 'wheel':
+ _handleWheelEvent(e);
+ _applyTranslation(imagePreviewsWrapper, ttN_FullscreenImageIndex, imageList);
+ break;
+ case 'keydown':
+ if (ARROW_KEYS.includes(e.code)) {
+ _handleArrowKeys(e);
+ _applyTranslation(imagePreviewsWrapper, ttN_FullscreenImageIndex, imageList);
+ } else if (e.code === 'Escape') {
+ _handleEscapeKey(e);
+ _applyTranslation(imagePreviewsWrapper, ttN_FullscreenImageIndex, imageList);
+ }
+ break;
+ }
+}
+
+function updateImageTLDE() {
+ for (let node of app.graph._nodes) {
+ if (!node.imgs) continue
+
+ let imgSrc = _findFullImageSRC(node);
+ if (!imgSrc) continue;
+
+ _removeLatentPreviewImageSRC(node, imgSrc)
+
+ ttN_srcDict[node.id] = ttN_srcDict[node.id] || [];
+
+ let index = ttN_srcDict[node.id].length;
+
+ if (!ttN_srcDict[node.id].includes((index, imgSrc))) {
+ ttN_srcDict[node.id].push((index, imgSrc));
+ if (ttN_Slideshow) {
+ updateImageElements(index);
+ }
+ }
+
+ }
+ saveSrcDict();
+ updateImageElements();
+};
+
+function _getImageDivFromSrc(imgSrc, index) {
+ // If image element doesn't exist, create it
+ if (!ttN_imageElementsDict[imgSrc]) {
+ const imgWrapper = document.createElement('div');
+ imgWrapper.classList.add('ttN-imgWrapper');
+
+ const imgElement = document.createElement('img');
+ imgElement.src = imgSrc;
+ imgElement.classList.add('ttN-img');
+ imgWrapper.appendChild(imgElement);
+
+ imgElement.addEventListener('click', () => {
+ ttN_FullscreenImageIndex = index;
+ updateImageElements(index);
+ });
+
+ ttN_imageElementsDict[imgSrc] = imgWrapper;
+ }
+ return ttN_imageElementsDict[imgSrc];
+}
+
+function _removeLatentPreviewImageSRC(node, imgSrc) {
+ let latentPreviewSrc = _findLatentPreviewImageSRC(node);
+ if (imgSrc && latentPreviewSrc) {
+ for (let i in node.imgs) {
+ if(!node.imgs[i].src.includes("filename")) {
+ node.imgs.splice(i, 1);
+ }
+ }
+ }
+}
+
+function _handleLatentPreview(imgDivList) {
+ let latentPreview = _findLatentPreviewImageSRC(ttN_FullscreenNode);
+ if (latentPreview && !latentPreview.includes("filename") && ttN_FullscreenImageIndex === imgDivList.length - 1) {
+ ttN_FullscreenImage.src = latentPreview
+ }
+}
+
+function updateImageElements(indexOverride = null) {
+ if (!ttN_isFullscreen) return;
+
+ const srcList = ttN_srcDict[ttN_FullscreenNode.id] || null;
+ if (!srcList) return;
+
+ const fullscreenWrapper = document.getElementById(FULLSCREEN_WRAPPER_ID);
+ if (!fullscreenWrapper) return
+
+ const imgDivList = srcList.map((src, index) => _getImageDivFromSrc(src, index));
+
+ ttN_FullscreenImageIndex = indexOverride || ttN_FullscreenImageIndex
+ if ((ttN_FullscreenImageIndex > imgDivList.length - 1) || (ttN_FullscreenImageIndex === -1)) {
+ ttN_FullscreenImageIndex = imgDivList.length - 1
+ }
+ if (ttN_FullscreenImageIndex < -1) ttN_FullscreenImageIndex = 0
+
+ ttN_FullscreenImage.src = imgDivList[ttN_FullscreenImageIndex].children[0].src;
+
+ const previewsWrapper = document.getElementById(IMAGE_PREVIEWS_WRAPPER_ID);
+ if (!previewsWrapper) return
+
+ if (ttN_Slideshow) {
+ fullscreenWrapper.classList.add('ttN-slideshow')
+ _handleLatentPreview(imgDivList);
+ } else {
+ fullscreenWrapper.classList.remove('ttN-slideshow')
+ }
+
+ if (ttN_FullscreenImageIndex > imgDivList.length - 1) ttN_FullscreenImageIndex = imgDivList.length - 1;
+ if (ttN_FullscreenImageIndex < 0) ttN_FullscreenImageIndex = 0;
+
+ imgDivList.forEach((imgDiv, index) => {
+ if (previewsWrapper.children[index] != imgDiv) previewsWrapper.appendChild(imgDiv);
+
+ const orderValue = index - ttN_FullscreenImageIndex;
+ imgDiv.style.order = orderValue;
+
+ if (index < ttN_FullscreenImageIndex) {
+ // For images before the selected image
+ imgDiv.classList.remove('ttN-divSelected', 'ttN-divAfter');
+ imgDiv.children[0].classList.remove('ttN-imgSelected', 'ttN-imgAfter');
+
+ imgDiv.classList.add('ttN-divBefore');
+ //imgDiv.children[0].classList.add('ttN-imgBefore');
+ }
+ else if (index === ttN_FullscreenImageIndex) {
+ // For the selected image
+ imgDiv.classList.remove('ttN-divBefore', 'ttN-divAfter');
+ imgDiv.children[0].classList.remove('ttN-imgBefore', 'ttN-imgAfter');
+
+ imgDiv.classList.add('ttN-divSelected');
+ imgDiv.children[0].classList.add('ttN-imgSelected');
+ }
+ else if (index > ttN_FullscreenImageIndex) {
+ // For images after the selected image
+ imgDiv.classList.remove('ttN-divSelected', 'ttN-divBefore');
+ imgDiv.children[0].classList.remove('ttN-imgSelected', 'ttN-imgBefore');
+
+ imgDiv.classList.add('ttN-divAfter');
+ //imgDiv.children[0].classList.add('ttN-imgAfter');
+ }
+ });
+
+ _applyTranslation(previewsWrapper, ttN_FullscreenImageIndex, imgDivList);
+}
+
+api.addEventListener("status", _handleExecutedEvent);
+api.addEventListener("progress", _handleExecutedEvent);
+api.addEventListener("execution_cached", _handleExecutedEvent);
+api.addEventListener("reconnecting", _handleReconnectingEvent);
+
+app.registerExtension({
+ name: "comfy.ttN.fullscreen",
+ init() {
+ document.addEventListener("keydown", ttNfullscreenEventListener, true);
+ },
+ setup() {
+ LGraphCanvas.prototype.ttNcreateFullscreen = function (node) {
+ if (!node || ttN_isFullscreen) return;
+
+ document.addEventListener("wheel", ttNfullscreenEventListener, true);
+
+ const fullscreenWrapper = document.createElement('div');
+ fullscreenWrapper.id = FULLSCREEN_WRAPPER_ID;
+ fullscreenWrapper.classList.add('ttN-slideshow');
+ document.body.appendChild(fullscreenWrapper);
+
+ const fullscreenImage = new Image();
+ fullscreenImage.src = _findFullImageSRC(node) || _findLatentPreviewImageSRC(node) || '';
+ fullscreenImage.id = FULLSCREEN_IMAGE_ID;
+ fullscreenWrapper.appendChild(fullscreenImage);
+
+ const previewsWrapper = document.createElement('div');
+ previewsWrapper.id = IMAGE_PREVIEWS_WRAPPER_ID;
+ previewsWrapper.style.display = 'none';
+
+ fullscreenWrapper.appendChild(previewsWrapper);
+
+ _initiateFullscreen(fullscreenWrapper).then(() => {
+ ttN_isFullscreen = true;
+ ttN_FullscreenImage = fullscreenImage;
+ updateImageElements();
+ }).catch(err => {
+ console.error("Error attempting to enable full-screen mode:", err.message, err.name);
+ });
+
+ fullscreenWrapper.onfullscreenchange = function (event) {
+ if (!document.fullscreenElement) {
+ LGraphCanvas.prototype.ttNcloseFullscreen();
+ }
+ };
+ }
+
+ LGraphCanvas.prototype.ttNcloseFullscreen = function () {
+ if (!ttN_isFullscreen) return;
+
+ const comfyMenu = document.getElementsByClassName("comfy-menu")[0]
+ const litegraph = document.getElementsByClassName("litegraph")[0]
+ litegraph.append(comfyMenu);
+
+ const fullscreenWrapper = document.getElementById(FULLSCREEN_WRAPPER_ID);
+ document.body.removeChild(fullscreenWrapper);
+
+ document.removeEventListener("wheel", ttNfullscreenEventListener, true);
+
+ ttN_isFullscreen = false;
+ }
+
+ const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
+ LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
+ const options = getNodeMenuOptions.apply(this, arguments);
+ node.setDirtyCanvas(true, true);
+
+ options.splice(options.length - 1, 0,
+ {
+ content: "Fullscreen (ttN)",
+ callback: () => { LGraphCanvas.prototype.ttNcreateFullscreen(node) }
+ },
+ {
+ content: "Set Default Fullscreen Node (ttN)",
+ callback: function () {
+ let selectedNode = _getSelectedNode();
+ if (selectedNode) {
+ sessionStorage.setItem('Comfy.Settings.ttN.default_fullscreen_node', JSON.stringify(selectedNode.id));
+ }
+ }
+ },
+ {
+ content: "Clear Default Fullscreen Node (ttN)",
+ callback: function () {
+ sessionStorage.removeItem('Comfy.Settings.ttN.default_fullscreen_node');
+ }
+ },
+ null
+ );
+
+ return options;
+ };
+ }
+});
+
+var styleElement = document.createElement("style");
+const cssCode = `
+
+#ttN-FullscreenWrapper {
+ display: flex;
+ justify-content: center;
+ align-items: end;
+ background-color: #1f1f1f;
+}
+
+#ttN-FullscreenImage {
+ height: inherit;
+ position: absolute;
+}
+
+#ttN-imagePreviewsWrapper {
+ position: absolute;
+ width: max-content;
+ z-index: 1;
+ height: 14vh;
+ left: 50vw;
+ transition: transform 0.2s ease;
+}
+
+.ttN-imgWrapper {
+ position: sticky;
+ transition: transform 0.2s ease;
+ align-self: end;
+}
+
+.ttN-img {
+ height: 121px;
+ margin: 7px;
+ cursor: pointer;
+ display: block;
+ border: 3px solid rgba(255,255,255);
+ box-shadow: 0px 0px 0px 10px;
+ transition: all 0.4s ease;
+}
+
+.ttN-img:hover {
+ transform: scale(1.1);
+ z-index: 1;
+}
+
+.ttN-imgSelected {
+ height: 200px!important;
+ z-index: 1;
+ transition: 0.1s;
+}
+.ttN-slideshow {
+ background: black!important;
+}
+
+`;
+
+styleElement.innerHTML = cssCode
+document.head.appendChild(styleElement);
\ No newline at end of file
diff --git a/ComfyUI/web/extensions/tinyterraNodes/ttNinterface.js b/ComfyUI/web/extensions/tinyterraNodes/ttNinterface.js
new file mode 100644
index 0000000000000000000000000000000000000000..d30cf02d2c48a3faee5aa7596fc9dcd9acf8b5dd
--- /dev/null
+++ b/ComfyUI/web/extensions/tinyterraNodes/ttNinterface.js
@@ -0,0 +1,587 @@
+import { app } from "../../scripts/app.js";
+
+const customPipeLineLink = "#7737AA"
+const customPipeLineSDXLLink = "#0DC52B"
+const customIntLink = "#29699C"
+const customXYPlotLink = "#74DA5D"
+
+var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {};
+if (!customLinkColors["PIPE_LINE"] || !LGraphCanvas.link_type_colors["PIPE_LINE"]) {customLinkColors["PIPE_LINE"] = customPipeLineLink;}
+if (!customLinkColors["PIPE_LINE_SDXL"] || !LGraphCanvas.link_type_colors["PIPE_LINE_SDXL"]) {customLinkColors["PIPE_LINE_SDXL"] = customPipeLineSDXLLink;}
+if (!customLinkColors["INT"] || !LGraphCanvas.link_type_colors["INT"]) {customLinkColors["INT"] = customIntLink;}
+if (!customLinkColors["XYPLOT"] || !LGraphCanvas.link_type_colors["XYPLOT"]) {customLinkColors["XYPLOT"] = customXYPlotLink;}
+
+localStorage.setItem('Comfy.Settings.ttN.customLinkColors', JSON.stringify(customLinkColors));
+
+let ttNisFullscreen = false;
+let ttNcurrentFullscreenImage = null;
+let ttNcurrentNode = null;
+
+app.registerExtension({
+ name: "comfy.ttN.interface",
+ init() {
+ function adjustToGrid(val, gridSize) {
+ return Math.round(val / gridSize) * gridSize;
+ }
+
+ function moveNodeBasedOnKey(e, node, gridSize, shiftMult) {
+ switch (e.code) {
+ case 'ArrowUp':
+ node.pos[1] -= gridSize * shiftMult;
+ break;
+ case 'ArrowDown':
+ node.pos[1] += gridSize * shiftMult;
+ break;
+ case 'ArrowLeft':
+ node.pos[0] -= gridSize * shiftMult;
+ break;
+ case 'ArrowRight':
+ node.pos[0] += gridSize * shiftMult;
+ break;
+ }
+ node.setDirtyCanvas(true, true);
+ }
+
+ function keyMoveNode(e, node) {
+ let gridSize = JSON.parse(localStorage.getItem('Comfy.Settings.Comfy.SnapToGrid.GridSize'));
+ gridSize = gridSize ? parseInt(gridSize) : 1;
+ let shiftMult = e.shiftKey ? 10 : 1;
+
+ node.pos[0] = adjustToGrid(node.pos[0], gridSize);
+ node.pos[1] = adjustToGrid(node.pos[1], gridSize);
+
+ moveNodeBasedOnKey(e, node, gridSize, shiftMult);
+ }
+
+ function getSelectedNodes(e) {
+ const inputField = e.composedPath()[0];
+ if (inputField.tagName === "TEXTAREA") return;
+ if (e.ctrlKey && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) {
+ let graphcanvas = LGraphCanvas.active_canvas;
+ for (let node in graphcanvas.selected_nodes) {
+ keyMoveNode(e, graphcanvas.selected_nodes[node]);
+ }
+ }
+ }
+
+ window.addEventListener("keydown", getSelectedNodes, true);
+
+ LGraphCanvas.prototype.ttNcreateDialog = function (htmlContent, onOK, onCancel) {
+ var dialog = document.createElement("div");
+ dialog.is_modified = false;
+ dialog.className = "ttN-dialog";
+ dialog.innerHTML = htmlContent + "";
+
+ dialog.close = function() {
+ if (dialog.parentNode) {
+ dialog.parentNode.removeChild(dialog);
+ }
+ };
+
+ var inputs = Array.from(dialog.querySelectorAll("input, select"));
+
+ inputs.forEach(input => {
+ input.addEventListener("keydown", function(e) {
+ dialog.is_modified = true;
+ if (e.keyCode == 27) { // ESC
+ onCancel && onCancel();
+ dialog.close();
+ } else if (e.keyCode == 13) { // Enter
+ onOK && onOK(dialog, inputs.map(input => input.value));
+ dialog.close();
+ } else if (e.keyCode != 13 && e.target.localName != "textarea") {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ });
+
+ var graphcanvas = LGraphCanvas.active_canvas;
+ var canvas = graphcanvas.canvas;
+
+ var rect = canvas.getBoundingClientRect();
+ var offsetx = -20;
+ var offsety = -20;
+ if (rect) {
+ offsetx -= rect.left;
+ offsety -= rect.top;
+ }
+
+ if (event) {
+ dialog.style.left = event.clientX + offsetx + "px";
+ dialog.style.top = event.clientY + offsety + "px";
+ } else {
+ dialog.style.left = canvas.width * 0.5 + offsetx + "px";
+ dialog.style.top = canvas.height * 0.5 + offsety + "px";
+ }
+
+ var button = dialog.querySelector("#ok");
+ button.addEventListener("click", function() {
+ onOK && onOK(dialog, inputs.map(input => input.value));
+ dialog.close();
+ });
+
+ canvas.parentNode.appendChild(dialog);
+
+ if(inputs) inputs[0].focus();
+
+ var dialogCloseTimer = null;
+ dialog.addEventListener("mouseleave", function(e) {
+ if(LiteGraph.dialog_close_on_mouse_leave)
+ if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave)
+ dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close();
+ });
+ dialog.addEventListener("mouseenter", function(e) {
+ if(LiteGraph.dialog_close_on_mouse_leave)
+ if(dialogCloseTimer) clearTimeout(dialogCloseTimer);
+ });
+
+ return dialog;
+ };
+
+ LGraphCanvas.prototype.ttNsetNodeDimension = function (node) {
+ const nodeWidth = node.size[0];
+ const nodeHeight = node.size[1];
+
+ let input_html = "";
+ input_html += "";
+
+ LGraphCanvas.prototype.ttNcreateDialog("Width/Height" + input_html,
+ function(dialog, values) {
+ var widthValue = Number(values[0]) ? values[0] : nodeWidth;
+ var heightValue = Number(values[1]) ? values[1] : nodeHeight;
+ let sz = node.computeSize();
+ node.setSize([Math.max(sz[0], widthValue), Math.max(sz[1], heightValue)]);
+ if (dialog.parentNode) {
+ dialog.parentNode.removeChild(dialog);
+ }
+ node.setDirtyCanvas(true, true);
+ },
+ null
+ );
+ };
+
+ LGraphCanvas.prototype.ttNsetSlotTypeColor = function(slot){
+ var slotColor = LGraphCanvas.link_type_colors[slot.output.type].toUpperCase();
+ var slotType = slot.output.type;
+ // Check if the color is in the correct format
+ if (!/^#([0-9A-F]{3}){1,2}$/i.test(slotColor)) {
+ slotColor = "#FFFFFF";
+ }
+
+ // Check if browser supports color input type
+ var inputType = "color";
+ var inputID = " id='colorPicker'";
+ var inputElem = document.createElement("input");
+ inputElem.setAttribute("type", inputType);
+ if (inputElem.type !== "color") {
+ // If it doesn't, fall back to text input
+ inputType = "text";
+ inputID = " ";
+ }
+
+ let input_html = "";
+ input_html += ""; // Add a default button
+ input_html += ""; // Add a reset button
+
+ var dialog = LGraphCanvas.prototype.ttNcreateDialog("" + slotType + "" +
+ input_html,
+ function(dialog, values){
+ var hexColor = values[0].toUpperCase();
+
+ if (!/^#([0-9A-F]{3}){1,2}$/i.test(hexColor)) {
+ return
+ }
+
+ if (hexColor === slotColor) {
+ return
+ }
+
+ var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {};
+ if (!customLinkColors[slotType + "_ORIG"]) {customLinkColors[slotType + "_ORIG"] = slotColor};
+ customLinkColors[slotType] = hexColor;
+ localStorage.setItem('Comfy.Settings.ttN.customLinkColors', JSON.stringify(customLinkColors));
+
+ app.canvas.default_connection_color_byType[slotType] = hexColor;
+ LGraphCanvas.link_type_colors[slotType] = hexColor;
+ }
+ );
+
+ var resetButton = dialog.querySelector("#reset");
+ resetButton.addEventListener("click", function() {
+ var colorInput = dialog.querySelector("input[type='" + inputType + "']");
+ colorInput.value = slotColor;
+ });
+
+ var defaultButton = dialog.querySelector("#Default");
+ defaultButton.addEventListener("click", function() {
+ var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {};
+ if (customLinkColors[slotType+"_ORIG"]) {
+ app.canvas.default_connection_color_byType[slotType] = customLinkColors[slotType+"_ORIG"];
+ LGraphCanvas.link_type_colors[slotType] = customLinkColors[slotType+"_ORIG"];
+
+ delete customLinkColors[slotType+"_ORIG"];
+ delete customLinkColors[slotType];
+ }
+ localStorage.setItem('Comfy.Settings.ttN.customLinkColors', JSON.stringify(customLinkColors));
+ dialog.close()
+ })
+
+ var colorPicker = dialog.querySelector("input[type='" + inputType + "']");
+ colorPicker.addEventListener("focusout", function(e) {
+ this.focus();
+ });
+ };
+
+ LGraphCanvas.prototype.ttNdefaultBGcolor = function(node, defaultBGColor){
+ setTimeout(() => {
+ if (defaultBGColor !== 'default' && !node.color) {
+ node.addProperty('ttNbgOverride', defaultBGColor);
+ node.color=defaultBGColor.color;
+ node.bgcolor=defaultBGColor.bgcolor;
+ }
+
+ if (node.color && node.properties.ttNbgOverride) {
+ if (node.properties.ttNbgOverride !== defaultBGColor && node.color === node.properties.ttNbgOverride.color) {
+ if (defaultBGColor === 'default') {
+ delete node.properties.ttNbgOverride
+ delete node.color
+ delete node.bgcolor
+ } else {
+ node.properties.ttNbgOverride = defaultBGColor
+ node.color=defaultBGColor.color;
+ node.bgcolor=defaultBGColor.bgcolor;
+ }
+ }
+
+ if (node.properties.ttNbgOverride !== defaultBGColor && node.color !== node.properties.ttNbgOverride?.color) {
+ delete node.properties.ttNbgOverride
+ }
+ }
+ }, 0);
+ };
+
+ LGraphCanvas.ttNonShowLinkStyles = function(value, options, e, menu, node) {
+ new LiteGraph.ContextMenu(
+ LiteGraph.LINK_RENDER_MODES,
+ { event: e, callback: inner_clicked, parentMenu: menu, node: node }
+ );
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+ var kV = Object.values(LiteGraph.LINK_RENDER_MODES).indexOf(v);
+
+ localStorage.setItem('Comfy.Settings.Comfy.LinkRenderMode', JSON.stringify(String(kV)));
+
+ app.canvas.links_render_mode = kV;
+ app.graph.setDirtyCanvas(true);
+ }
+
+ return false;
+ };
+
+ LGraphCanvas.ttNlinkStyleBorder = function(value, options, e, menu, node) {
+ new LiteGraph.ContextMenu(
+ [false, true],
+ { event: e, callback: inner_clicked, parentMenu: menu, node: node }
+ );
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+
+ localStorage.setItem('Comfy.Settings.ttN.links_render_border', JSON.stringify(v));
+
+ app.canvas.render_connections_border = v;
+ }
+
+ return false;
+ };
+
+ LGraphCanvas.ttNlinkStyleShadow = function(value, options, e, menu, node) {
+ new LiteGraph.ContextMenu(
+ [false, true],
+ { event: e, callback: inner_clicked, parentMenu: menu, node: node }
+ );
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+
+ localStorage.setItem('Comfy.Settings.ttN.links_render_shadow', JSON.stringify(v));
+
+ app.canvas.render_connections_shadows = v;
+ }
+
+ return false;
+ };
+
+ LGraphCanvas.ttNsetDefaultBGColor = function(value, options, e, menu, node) {
+ if (!node) {
+ throw "no node for color";
+ }
+
+ var values = [];
+ values.push({
+ value: null,
+ content:
+ "No Color"
+ });
+
+ for (var i in LGraphCanvas.node_colors) {
+ var color = LGraphCanvas.node_colors[i];
+ var value = {
+ value: i,
+ content:
+ "" +
+ i +
+ ""
+ };
+ values.push(value);
+ }
+ new LiteGraph.ContextMenu(values, {
+ event: e,
+ callback: inner_clicked,
+ parentMenu: menu,
+ node: node
+ });
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+
+ var defaultBGColor = v.value ? LGraphCanvas.node_colors[v.value] : 'default';
+
+ localStorage.setItem('Comfy.Settings.ttN.defaultBGColor', JSON.stringify(defaultBGColor));
+
+ for (var i in app.graph._nodes) {
+ LGraphCanvas.prototype.ttNdefaultBGcolor(app.graph._nodes[i], defaultBGColor);
+ }
+
+ node.setDirtyCanvas(true, true);
+ }
+
+ return false;
+ };
+
+ LGraphCanvas.ttNshowExecutionOrder = function(value, options, e, menu, node) {
+ var values = [];
+ values.push({
+ value: true,
+ content:
+ "True"
+ },
+ {
+ value: false,
+ content:
+ "False"
+ }
+ );
+
+ new LiteGraph.ContextMenu(values, {
+ event: e,
+ callback: inner_clicked,
+ parentMenu: menu,
+ node: node
+ });
+
+ function inner_clicked(v) {
+ var showExecOrder = v.value ? v.value : false;
+
+ localStorage.setItem('Comfy.Settings.ttN.showExecutionOrder', JSON.stringify(showExecOrder));
+
+ LGraphCanvas.active_canvas.render_execution_order = showExecOrder;
+
+ node.setDirtyCanvas(true, true);
+ }
+
+ return false;
+ };
+
+ const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
+ LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
+ const options = getNodeMenuOptions.apply(this, arguments);
+ node.setDirtyCanvas(true, true);
+
+ options.splice(options.length - 1, 0,
+ {
+ content: "Node Dimensions (ttN)",
+ callback: () => { LGraphCanvas.prototype.ttNsetNodeDimension(node); }
+ },
+ {
+ content: "Default BG Color (ttN)",
+ has_submenu: true,
+ callback: LGraphCanvas.ttNsetDefaultBGColor
+ },
+ {
+ content: "Show Execution Order (ttN)",
+ has_submenu: true,
+ callback: LGraphCanvas.ttNshowExecutionOrder
+
+ },
+ null
+ )
+
+ return options;
+ };
+
+ LGraphCanvas.prototype.ttNupdateRenderSettings = function (app) {
+ let showLinkBorder = Number(localStorage.getItem('Comfy.Settings.ttN.links_render_border'));
+ if (showLinkBorder !== undefined) {app.canvas.render_connections_border = showLinkBorder}
+
+ let showLinkShadow = Number(localStorage.getItem('Comfy.Settings.ttN.links_render_shadow'));
+ if (showLinkShadow !== undefined) {app.canvas.render_connections_shadows = showLinkShadow}
+
+ let showExecOrder = localStorage.getItem('Comfy.Settings.ttN.showExecutionOrder');
+ if (showExecOrder === 'true') {app.canvas.render_execution_order = true}
+ else {app.canvas.render_execution_order = false}
+
+ var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {};
+ Object.assign(app.canvas.default_connection_color_byType, customLinkColors);
+ Object.assign(LGraphCanvas.link_type_colors, customLinkColors);
+ }
+ },
+
+ beforeRegisterNodeDef(nodeType, nodeData, app) {
+ nodeType.prototype.getSlotMenuOptions = (slot) => {
+ let menu_info = [];
+ if (
+ slot &&
+ slot.output &&
+ slot.output.links &&
+ slot.output.links.length
+ ) {
+ menu_info.push({ content: "Disconnect Links", slot: slot });
+ }
+ var _slot = slot.input || slot.output;
+ if (_slot.removable){
+ menu_info.push(
+ _slot.locked
+ ? "Cannot remove"
+ : { content: "Remove Slot", slot: slot }
+ );
+ }
+ if (!_slot.nameLocked){
+ menu_info.push({ content: "Rename Slot", slot: slot });
+ }
+
+ menu_info.push({ content: "Slot Type Color (ttN)", slot: slot, callback: () => { LGraphCanvas.prototype.ttNsetSlotTypeColor(slot) } });
+ menu_info.push({ content: "Show Link Border (ttN)", has_submenu: true, slot: slot, callback: LGraphCanvas.ttNlinkStyleBorder });
+ menu_info.push({ content: "Show Link Shadow (ttN)", has_submenu: true, slot: slot, callback: LGraphCanvas.ttNlinkStyleShadow });
+ menu_info.push({ content: "Link Style (ttN)", has_submenu: true, slot: slot, callback: LGraphCanvas.ttNonShowLinkStyles });
+
+ return menu_info;
+ }
+ },
+
+ setup() {
+ LGraphCanvas.prototype.ttNupdateRenderSettings(app);
+ },
+ nodeCreated(node) {
+ let defaultBGColor = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.defaultBGColor'));
+ if (defaultBGColor) {LGraphCanvas.prototype.ttNdefaultBGcolor(node, defaultBGColor)};
+ },
+ loadedGraphNode(node, app) {
+ LGraphCanvas.prototype.ttNupdateRenderSettings(app);
+
+ let defaultBGColor = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.defaultBGColor'));
+ if (defaultBGColor) {LGraphCanvas.prototype.ttNdefaultBGcolor(node, defaultBGColor)};
+ },
+});
+
+var styleElement = document.createElement("style");
+const cssCode = `
+.ttN-dialog {
+ top: 10px;
+ left: 10px;
+ min-height: 1em;
+ background-color: var(--comfy-menu-bg);
+ font-size: 1.2em;
+ box-shadow: 0 0 7px black !important;
+ z-index: 10;
+ display: grid;
+ border-radius: 7px;
+ padding: 7px 7px;
+ position: fixed;
+}
+.ttN-dialog .name {
+ display: inline-block;
+ min-height: 1.5em;
+ font-size: 14px;
+ font-family: sans-serif;
+ color: var(--descrip-text);
+ padding: 0;
+ vertical-align: middle;
+ justify-self: center;
+}
+.ttN-dialog input,
+.ttN-dialog textarea,
+.ttN-dialog select {
+ margin: 3px;
+ min-width: 60px;
+ min-height: 1.5em;
+ background-color: var(--comfy-input-bg);
+ border: 2px solid;
+ border-color: var(--border-color);
+ color: var(--input-text);
+ border-radius: 14px;
+ padding-left: 10px;
+ outline: none;
+}
+
+.ttN-dialog #colorPicker {
+ margin: 0px;
+ min-width: 100%;
+ min-height: 2.5em;
+ border-radius: 0px;
+ padding: 0px 2px 0px 2px;
+ border: unset;
+}
+
+.ttN-dialog textarea {
+ min-height: 150px;
+}
+
+.ttN-dialog button {
+ margin-top: 3px;
+ vertical-align: top;
+ background-color: #999;
+ border: 0;
+ padding: 4px 18px;
+ border-radius: 20px;
+ cursor: pointer;
+}
+
+.ttN-dialog button.rounded,
+.ttN-dialog input.rounded {
+ border-radius: 0 12px 12px 0;
+}
+
+.ttN-dialog .helper {
+ overflow: auto;
+ max-height: 200px;
+}
+
+.ttN-dialog .help-item {
+ padding-left: 10px;
+}
+
+.ttN-dialog .help-item:hover,
+.ttN-dialog .help-item.selected {
+ cursor: pointer;
+ background-color: white;
+ color: black;
+}
+`
+styleElement.innerHTML = cssCode
+document.head.appendChild(styleElement);
\ No newline at end of file
diff --git a/ComfyUI/web/extensions/tinyterraNodes/ttNwidgets.js b/ComfyUI/web/extensions/tinyterraNodes/ttNwidgets.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e9ef42784d9113a86b8d1b3c3efab878bd20332
--- /dev/null
+++ b/ComfyUI/web/extensions/tinyterraNodes/ttNwidgets.js
@@ -0,0 +1,403 @@
+import { app } from "../../scripts/app.js";
+import { ComfyWidgets } from "../../scripts/widgets.js";
+
+const KEY_CODES = { ENTER: 13, ESC: 27, ARROW_DOWN: 40, ARROW_UP: 38 };
+const WIDGET_GAP = -4;
+
+function hideInfoWidget(e, node, widget) {
+ let dropdownShouldBeRemoved = false;
+ let selectionIndex = -1;
+
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ displayDropdown(widget);
+ } else {
+ hideWidget(widget, node);
+ }
+
+ function createDropdownElement() {
+ const dropdown = document.createElement('ul');
+ dropdown.id = 'hideinfo-dropdown';
+ dropdown.setAttribute('role', 'listbox');
+ dropdown.classList.add('hideInfo-dropdown');
+ return dropdown;
+ }
+
+ function createDropdownItem(textContent, action) {
+ const listItem = document.createElement('li');
+ listItem.id = `hideInfo-item-${textContent.replace(/ /g, '')}`;
+ listItem.classList.add('hideInfo-item');
+ listItem.setAttribute('role', 'option');
+ listItem.textContent = textContent;
+ listItem.addEventListener('mousedown', (event) => {
+ event.preventDefault();
+ action(widget, node); // perform the action when dropdown item is clicked
+ removeDropdown();
+ dropdownShouldBeRemoved = false;
+ });
+ listItem.dataset.action = textContent.replace(/ /g, ''); // store the action in a data attribute
+ return listItem;
+ }
+
+ function displayDropdown(widget) {
+ removeDropdown();
+
+ const dropdown = createDropdownElement();
+ const listItemHide = createDropdownItem('Hide info Widget', hideWidget);
+ const listItemHideAll = createDropdownItem('Hide for all of this node-type', hideWidgetForNodetype);
+
+ dropdown.appendChild(listItemHide);
+ dropdown.appendChild(listItemHideAll);
+
+ const inputRect = widget.inputEl.getBoundingClientRect();
+ dropdown.style.top = `${inputRect.top + inputRect.height}px`;
+ dropdown.style.left = `${inputRect.left}px`;
+ dropdown.style.width = `${inputRect.width}px`;
+
+ document.body.appendChild(dropdown);
+ dropdownShouldBeRemoved = true;
+
+ widget.inputEl.removeEventListener('keydown', handleKeyDown);
+ widget.inputEl.addEventListener('keydown', handleKeyDown);
+ document.addEventListener('click', handleDocumentClick);
+ }
+
+ function removeDropdown() {
+ const dropdown = document.getElementById('hideinfo-dropdown');
+ if (dropdown) {
+ dropdown.remove();
+ widget.inputEl.removeEventListener('keydown', handleKeyDown);
+ }
+ document.removeEventListener('click', handleDocumentClick);
+
+ }
+
+ function handleKeyDown(event) {
+ const dropdownItems = document.querySelectorAll('.hideInfo-item');
+
+ if (event.keyCode === KEY_CODES.ENTER && dropdownShouldBeRemoved) {
+ event.preventDefault();
+ if (selectionIndex !== -1) {
+ const selectedAction = dropdownItems[selectionIndex].dataset.action;
+ if (selectedAction === 'HideinfoWidget') {
+ hideWidget(widget, node);
+ } else if (selectedAction === 'Hideforall') {
+ hideWidgetForNodetype(widget, node);
+ }
+ removeDropdown();
+ dropdownShouldBeRemoved = false;
+ }
+ } else if (event.keyCode === KEY_CODES.ARROW_DOWN && dropdownShouldBeRemoved) {
+ event.preventDefault();
+ if (selectionIndex !== -1) {
+ dropdownItems[selectionIndex].classList.remove('selected');
+ }
+ selectionIndex = (selectionIndex + 1) % dropdownItems.length;
+ dropdownItems[selectionIndex].classList.add('selected');
+ } else if (event.keyCode === KEY_CODES.ARROW_UP && dropdownShouldBeRemoved) {
+ event.preventDefault();
+ if (selectionIndex !== -1) {
+ dropdownItems[selectionIndex].classList.remove('selected');
+ }
+ selectionIndex = (selectionIndex - 1 + dropdownItems.length) % dropdownItems.length;
+ dropdownItems[selectionIndex].classList.add('selected');
+ } else if (event.keyCode === KEY_CODES.ESC && dropdownShouldBeRemoved) {
+ event.preventDefault();
+ removeDropdown();
+ }
+ }
+
+ function hideWidget(widget, node) {
+ node.properties['infoWidgetHidden'] = true;
+ widget.type = "ttNhidden";
+ widget.computeSize = () => [0, WIDGET_GAP];
+ node.setSize([node.size[0], node.size[1]]);
+ }
+
+ function hideWidgetForNodetype(widget, node) {
+ hideWidget(widget, node)
+ const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]");
+ if (!hiddenNodeTypes.includes(node.constructor.type)) {
+ hiddenNodeTypes.push(node.constructor.type);
+ }
+ localStorage.setItem('hiddenWidgetNodeTypes', JSON.stringify(hiddenNodeTypes));
+ }
+
+ function handleDocumentClick(event) {
+ const dropdown = document.getElementById('hideinfo-dropdown');
+
+ // If the click was outside the dropdown and the dropdown should be removed, remove it
+ if (dropdown && !dropdown.contains(event.target) && dropdownShouldBeRemoved) {
+ removeDropdown();
+ dropdownShouldBeRemoved = false;
+ }
+ }
+}
+
+
+var styleElement = document.createElement("style");
+const cssCode = `
+.ttN-info_widget {
+ background-color: var(--comfy-input-bg);
+ color: var(--input-text);
+ overflow: hidden;
+ padding: 2px;
+ resize: none;
+ border: none;
+ box-sizing: border-box;
+ font-size: 10px;
+ border-radius: 7px;
+ text-align: center;
+ text-wrap: balance;
+ text-transform: uppercase;
+}
+.hideInfo-dropdown {
+ position: absolute;
+ box-sizing: border-box;
+ background-color: #121212;
+ border-radius: 7px;
+ box-shadow: 0 2px 4px rgba(255, 255, 255, .25);
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ z-index: 1000;
+ overflow: auto;
+ max-height: 200px;
+}
+
+.hideInfo-dropdown li {
+ padding: 4px 10px;
+ cursor: pointer;
+ font-family: system-ui;
+ font-size: 0.7rem;
+}
+
+.hideInfo-dropdown li:hover,
+.hideInfo-dropdown li.selected {
+ background-color: #e5e5e5;
+ border-radius: 7px;
+}
+`
+styleElement.innerHTML = cssCode
+document.head.appendChild(styleElement);
+
+const InfoSymbol = Symbol();
+const InfoResizeSymbol = Symbol();
+
+
+
+
+// WIDGET FUNCTIONS
+function addInfoWidget(node, name, opts, app) {
+ const INFO_W_SIZE = 50;
+
+ node.addProperty('infoWidgetHidden', false)
+
+ function computeSize(size) {
+ if (node.widgets[0].last_y == null) return;
+
+ let y = node.widgets[0].last_y;
+
+ // Compute the height of all non ttNinfo widgets
+ let widgetHeight = 0;
+ const infoWidges = [];
+ for (let i = 0; i < node.widgets.length; i++) {
+ const w = node.widgets[i];
+ if (w.type === "ttNinfo") {
+ infoWidges.push(w);
+ } else {
+ if (w.computeSize) {
+ widgetHeight += w.computeSize()[1] + 4;
+ } else {
+ widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
+ }
+ }
+ }
+
+ let infoWidgetSpace = infoWidges.length * INFO_W_SIZE; // Height for all info widgets
+
+ // Check if there's enough space for all widgets
+ if (size[1] < y + widgetHeight + infoWidgetSpace) {
+ // There isn't enough space for all the widgets, increase the size of the node
+ node.size[1] = y + widgetHeight + infoWidgetSpace;
+ node.graph.setDirtyCanvas(true);
+ }
+
+ // Position each of the widgets
+ for (const w of node.widgets) {
+ w.y = y;
+ if (w.type === "ttNinfo") {
+ y += INFO_W_SIZE;
+ } else if (w.computeSize) {
+ y += w.computeSize()[1] + 4;
+ } else {
+ y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
+ }
+ }
+ }
+
+ const widget = {
+ type: "ttNinfo",
+ name,
+ get value() {
+ return this.inputEl.value;
+ },
+ set value(x) {
+ this.inputEl.value = x;
+ },
+ draw: function (ctx, _, widgetWidth, y, widgetHeight) {
+ if (!this.parent.inputHeight) {
+ // If we are initially offscreen when created we wont have received a resize event
+ // Calculate it here instead
+ computeSize(node.size);
+ }
+ const visible = app.canvas.ds.scale > 0.5 && this.type === "ttNinfo";
+ const margin = 10;
+ const elRect = ctx.canvas.getBoundingClientRect();
+ const transform = new DOMMatrix()
+ .scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
+ .multiplySelf(ctx.getTransform())
+ .translateSelf(margin, margin + y);
+
+ Object.assign(this.inputEl.style, {
+ transformOrigin: "0 0",
+ transform: transform,
+ left: "0px",
+ top: "0px",
+ width: `${widgetWidth - (margin * 2)}px`,
+ height: `${this.parent.inputHeight - (margin * 2)}px`,
+ position: "absolute",
+ background: (!node.color)?'':node.color,
+ color: (!node.color)?'':'white',
+ zIndex: app.graph._nodes.indexOf(node),
+ });
+ this.inputEl.hidden = !visible;
+ },
+ };
+ widget.inputEl = document.createElement("textarea");
+ widget.inputEl.className = "ttN-info_widget";
+ widget.inputEl.value = opts.defaultVal;
+ widget.inputEl.placeholder = opts.placeholder || "";
+ widget.inputEl.readOnly = true;
+ widget.parent = node;
+
+ document.body.appendChild(widget.inputEl);
+
+ node.addCustomWidget(widget);
+
+ app.canvas.onDrawBackground = function () {
+ // Draw node isnt fired once the node is off the screen
+ // if it goes off screen quickly, the input may not be removed
+ // this shifts it off screen so it can be moved back if the node is visible.
+ for (let n in app.graph._nodes) {
+ n = graph._nodes[n];
+ for (let w in n.widgets) {
+ let wid = n.widgets[w];
+ if (Object.hasOwn(wid, "inputEl")) {
+ wid.inputEl.style.left = -8000 + "px";
+ wid.inputEl.style.position = "absolute";
+ }
+ }
+ }
+ };
+
+ node.onRemoved = function () {
+ // When removing this node we need to remove the input from the DOM
+ for (let y in this.widgets) {
+ if (this.widgets[y].inputEl) {
+ this.widgets[y].inputEl.remove();
+ }
+ }
+ };
+
+ widget.onRemove = () => {
+ widget.inputEl?.remove();
+
+ // Restore original size handler if we are the last
+ if (!--node[InfoSymbol]) {
+ node.onResize = node[InfoResizeSymbol];
+ delete node[InfoSymbol];
+ delete node[InfoResizeSymbol];
+ }
+ };
+
+ if (node[InfoSymbol]) {
+ node[InfoSymbol]++;
+ } else {
+ node[InfoSymbol] = 1;
+ const onResize = (node[InfoResizeSymbol] = node.onResize);
+
+ node.onResize = function (size) {
+ computeSize(size);
+
+ // Call original resizer handler
+ if (onResize) {
+ console.log(this, arguments)
+ onResize.apply(this, arguments);
+ }
+ };
+ }
+
+ return { widget };
+}
+
+// WIDGETS
+const ttNcustomWidgets = {
+ INFO(node, inputName, inputData, app) {
+ const defaultVal = inputData[1].default || "";
+ return addInfoWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
+ },
+}
+
+
+
+app.registerExtension({
+ name: "comfy.ttN.widgets",
+ getCustomWidgets(app) {
+ return ttNcustomWidgets;
+ },
+ nodeCreated(node) {
+ if (node.widgets) {
+ // Locate info widgets
+ const widgets = node.widgets.filter(
+ (n) => (n.type === "ttNinfo")
+ );
+ for (const widget of widgets) {
+ widget.inputEl.addEventListener('contextmenu', function(e) {
+ hideInfoWidget(e, node, widget);
+ });
+ widget.inputEl.addEventListener('click', function(e) {
+ hideInfoWidget(e, node, widget);
+ });
+ }
+ }
+ },
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]");
+ const origOnConfigure = nodeType.prototype.onConfigure;
+ nodeType.prototype.onConfigure = function () {
+ const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined;
+ if (this.properties['infoWidgetHidden']) {
+ for (let i in this.widgets) {
+ if (this.widgets[i].type == "ttNinfo") {
+ hideInfoWidget(null, this, this.widgets[i]);
+ }
+ }
+ }
+ return r;
+ };
+ const origOnAdded = nodeType.prototype.onAdded;
+ nodeType.prototype.onAdded = function () {
+ const r = origOnAdded ? origOnAdded.apply(this, arguments) : undefined;
+ if (hiddenNodeTypes.includes(this.type)) {
+ for (let i in this.widgets) {
+ if (this.widgets[i].type == "ttNinfo") {
+ this.properties['infoWidgetHidden'] = true;
+ }
+ }
+ }
+ return r;
+ }
+ }
+});
\ No newline at end of file
diff --git a/ComfyUI/web/extensions/tinyterraNodes/ttNxyPlot.js b/ComfyUI/web/extensions/tinyterraNodes/ttNxyPlot.js
new file mode 100644
index 0000000000000000000000000000000000000000..84f03590e869694ee06b13c49d2e8255ecf7dde5
--- /dev/null
+++ b/ComfyUI/web/extensions/tinyterraNodes/ttNxyPlot.js
@@ -0,0 +1,212 @@
+import { app } from "../../scripts/app.js";
+import { ttN_CreateDropdown, ttN_RemoveDropdown } from "./ttN.js";
+
+function generateNumList(dictionary) {
+ const minimum = dictionary["min"] || 0;
+ const maximum = dictionary["max"] || 0;
+ const step = dictionary["step"] || 1;
+
+ if (step === 0) {
+ return [];
+ }
+
+ const result = [];
+ let currentValue = minimum;
+
+ while (currentValue <= maximum) {
+ if (Number.isInteger(step)) {
+ result.push(Math.round(currentValue) + '; ');
+ } else {
+ let formattedValue = currentValue.toFixed(3);
+ if(formattedValue == -0.000){
+ formattedValue = '0.000';
+ }
+ if (!/\.\d{3}$/.test(formattedValue)) {
+ formattedValue += "0";
+ }
+ result.push(formattedValue + "; ");
+ }
+ currentValue += step;
+ }
+
+ if (maximum >= 0 && minimum >= 0) {
+ //low to high
+ return result;
+ }
+ else {
+ //high to low
+ return result.reverse();
+ }
+}
+
+let plotDict = {};
+let currentOptionsDict = {};
+
+function getCurrentOptionLists(node, widget) {
+ const nodeId = String(node.id);
+ const widgetName = widget.name;
+ const widgetValue = widget.value.replace(/^(loader|sampler):\s/, '');
+
+ if (!currentOptionsDict[nodeId] || !currentOptionsDict[nodeId][widgetName]) {
+ currentOptionsDict[nodeId] = {...currentOptionsDict[nodeId], [widgetName]: plotDict[widgetValue]};
+ } else if (currentOptionsDict[nodeId][widgetName] != plotDict[widgetValue]) {
+ currentOptionsDict[nodeId][widgetName] = plotDict[widgetValue];
+ }
+}
+
+function addGetSetters(node) {
+ if (node.widgets)
+ for (const w of node.widgets) {
+ if (w.name === "x_axis" ||
+ w.name === "y_axis") {
+ let widgetValue = w.value;
+
+ // Define getters and setters for widget values
+ Object.defineProperty(w, 'value', {
+
+ get() {
+ return widgetValue;
+ },
+ set(newVal) {
+ if (newVal !== widgetValue) {
+ widgetValue = newVal;
+ getCurrentOptionLists(node, w);
+ }
+ }
+ });
+ }
+ }
+}
+
+function dropdownCreator(node) {
+ if (node.widgets) {
+ const widgets = node.widgets.filter(
+ (n) => (n.type === "customtext" && n.dynamicPrompts !== false) || n.dynamicPrompts
+ );
+
+ for (const w of widgets) {
+ function replaceOptionSegments(selectedOption, inputSegments, cursorSegmentIndex, optionsList) {
+ if (selectedOption) {
+ inputSegments[cursorSegmentIndex] = selectedOption;
+ }
+
+ return inputSegments.map(segment => verifySegment(segment, optionsList))
+ .filter(item => item !== '')
+ .join('');
+ }
+
+ function verifySegment(segment, optionsList) {
+ segment = cleanSegment(segment);
+
+ if (isInOptionsList(segment, optionsList)) {
+ return segment + '; ';
+ }
+
+ let matchedOptions = findMatchedOptions(segment, optionsList);
+
+ if (matchedOptions.length === 1 || matchedOptions.length === 2) {
+ return matchedOptions[0];
+ }
+
+ if (isInOptionsList(formatNumberSegment(segment), optionsList)) {
+ return formatNumberSegment(segment) + '; ';
+ }
+
+ return '';
+ }
+
+ function cleanSegment(segment) {
+ return segment.replace(/(\n|;| )/g, '');
+ }
+
+ function isInOptionsList(segment, optionsList) {
+ return optionsList.includes(segment + '; ');
+ }
+
+ function findMatchedOptions(segment, optionsList) {
+ return optionsList.filter(option => option.toLowerCase().includes(segment.toLowerCase()));
+ }
+
+ function formatNumberSegment(segment) {
+ if (Number(segment)) {
+ return Number(segment).toFixed(3);
+ }
+
+ if (['0', '0.', '0.0', '0.00', '00'].includes(segment)) {
+ return '0.000';
+ }
+ return segment;
+ }
+
+
+ const onInput = function () {
+ const nodeId = String(w.parent.id);
+ const axisWidgetName = w.name[0] + '_axis';
+
+ let optionsList = currentOptionsDict[nodeId]?.[axisWidgetName] || [];
+ if (optionsList.length === 0) {return}
+
+ const inputText = w.inputEl.value;
+ const cursorPosition = w.inputEl.selectionStart;
+
+ let inputSegments = inputText.split('; ');
+
+ const cursorSegmentIndex = inputText.substring(0, cursorPosition).split('; ').length - 1;
+ const currentSegment = inputSegments[cursorSegmentIndex];
+ const currentSegmentLower = currentSegment.replace(/\n/g, '').toLowerCase();
+
+ const filteredOptionsList = optionsList.filter(option => option.toLowerCase().includes(currentSegmentLower)).map(option => option.replace(/; /g, ''));
+
+ if (filteredOptionsList.length > 0) {
+ ttN_CreateDropdown(w.inputEl, filteredOptionsList, (selectedOption) => {
+ const verifiedText = replaceOptionSegments(selectedOption, inputSegments, cursorSegmentIndex, optionsList);
+ w.inputEl.value = verifiedText;
+ });
+ }
+ else {
+ ttN_RemoveDropdown();
+ const verifiedText = replaceOptionSegments(null, inputSegments, cursorSegmentIndex, optionsList);
+ w.inputEl.value = verifiedText;
+ }
+ };
+
+ w.inputEl.removeEventListener('input', onInput);
+ w.inputEl.addEventListener('input', onInput);
+ w.inputEl.removeEventListener('mouseup', onInput);
+ w.inputEl.addEventListener('mouseup', onInput);
+ }
+ }
+}
+
+app.registerExtension({
+ name: "comfy.ttN.xyPlot",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === "ttN xyPlot") {
+ plotDict = nodeData.input.hidden.plot_dict[0];
+
+ for (const key in plotDict) {
+ const value = plotDict[key];
+ if (Array.isArray(value)) {
+ let updatedValues = [];
+ for (const v of value) {
+ updatedValues.push(v + '; ');
+ }
+ plotDict[key] = updatedValues;
+ } else if (typeof(value) === 'object') {
+ plotDict[key] = generateNumList(value);
+ } else {
+ plotDict[key] = value + '; ';
+ }
+ }
+ plotDict["None"] = [];
+ plotDict["---------------------"] = [];
+ }
+ },
+ nodeCreated(node) {
+ if (node.getTitle() === "xyPlot") {
+ addGetSetters(node);
+ dropdownCreator(node);
+
+ }
+ }
+});
\ No newline at end of file
diff --git a/ComfyUI/web/index.html b/ComfyUI/web/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..41bc246c090e51824212f70796e508645affe9c8
--- /dev/null
+++ b/ComfyUI/web/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+ ComfyUI
+
+
+
+
+
+
+
+
+
+
diff --git a/ComfyUI/web/jsconfig.json b/ComfyUI/web/jsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..57403d8cf2b5ca7b5d2bf2a4345c2a031e97516f
--- /dev/null
+++ b/ComfyUI/web/jsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "/*": ["./*"]
+ }
+ },
+ "include": ["."]
+}
diff --git a/ComfyUI/web/lib/litegraph.core.js b/ComfyUI/web/lib/litegraph.core.js
new file mode 100644
index 0000000000000000000000000000000000000000..434c4a83bf1c2f3d91e52fbad2c9813a4c24b41c
--- /dev/null
+++ b/ComfyUI/web/lib/litegraph.core.js
@@ -0,0 +1,14413 @@
+//packer version
+
+
+(function(global) {
+ // *************************************************************
+ // LiteGraph CLASS *******
+ // *************************************************************
+
+ /**
+ * The Global Scope. It contains all the registered node classes.
+ *
+ * @class LiteGraph
+ * @constructor
+ */
+
+ var LiteGraph = (global.LiteGraph = {
+ VERSION: 0.4,
+
+ CANVAS_GRID_SIZE: 10,
+
+ NODE_TITLE_HEIGHT: 30,
+ NODE_TITLE_TEXT_Y: 20,
+ NODE_SLOT_HEIGHT: 20,
+ NODE_WIDGET_HEIGHT: 20,
+ NODE_WIDTH: 140,
+ NODE_MIN_WIDTH: 50,
+ NODE_COLLAPSED_RADIUS: 10,
+ NODE_COLLAPSED_WIDTH: 80,
+ NODE_TITLE_COLOR: "#999",
+ NODE_SELECTED_TITLE_COLOR: "#FFF",
+ NODE_TEXT_SIZE: 14,
+ NODE_TEXT_COLOR: "#AAA",
+ NODE_SUBTEXT_SIZE: 12,
+ NODE_DEFAULT_COLOR: "#333",
+ NODE_DEFAULT_BGCOLOR: "#353535",
+ NODE_DEFAULT_BOXCOLOR: "#666",
+ NODE_DEFAULT_SHAPE: "box",
+ NODE_BOX_OUTLINE_COLOR: "#FFF",
+ DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)",
+ DEFAULT_GROUP_FONT: 24,
+
+ WIDGET_BGCOLOR: "#222",
+ WIDGET_OUTLINE_COLOR: "#666",
+ WIDGET_TEXT_COLOR: "#DDD",
+ WIDGET_SECONDARY_TEXT_COLOR: "#999",
+
+ LINK_COLOR: "#9A9",
+ EVENT_LINK_COLOR: "#A86",
+ CONNECTING_LINK_COLOR: "#AFA",
+
+ MAX_NUMBER_OF_NODES: 10000, //avoid infinite loops
+ DEFAULT_POSITION: [100, 100], //default node position
+ VALID_SHAPES: ["default", "box", "round", "card"], //,"circle"
+
+ //shapes are used for nodes but also for slots
+ BOX_SHAPE: 1,
+ ROUND_SHAPE: 2,
+ CIRCLE_SHAPE: 3,
+ CARD_SHAPE: 4,
+ ARROW_SHAPE: 5,
+ GRID_SHAPE: 6, // intended for slot arrays
+
+ //enums
+ INPUT: 1,
+ OUTPUT: 2,
+
+ EVENT: -1, //for outputs
+ ACTION: -1, //for inputs
+
+ NODE_MODES: ["Always", "On Event", "Never", "On Trigger"], // helper, will add "On Request" and more in the future
+ NODE_MODES_COLORS:["#666","#422","#333","#224","#626"], // use with node_box_coloured_by_mode
+ ALWAYS: 0,
+ ON_EVENT: 1,
+ NEVER: 2,
+ ON_TRIGGER: 3,
+
+ UP: 1,
+ DOWN: 2,
+ LEFT: 3,
+ RIGHT: 4,
+ CENTER: 5,
+
+ LINK_RENDER_MODES: ["Straight", "Linear", "Spline"], // helper
+ STRAIGHT_LINK: 0,
+ LINEAR_LINK: 1,
+ SPLINE_LINK: 2,
+
+ NORMAL_TITLE: 0,
+ NO_TITLE: 1,
+ TRANSPARENT_TITLE: 2,
+ AUTOHIDE_TITLE: 3,
+ VERTICAL_LAYOUT: "vertical", // arrange nodes vertically
+
+ proxy: null, //used to redirect calls
+ node_images_path: "",
+
+ debug: false,
+ catch_exceptions: true,
+ throw_errors: true,
+ allow_scripts: false, //if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits
+ registered_node_types: {}, //nodetypes by string
+ node_types_by_file_extension: {}, //used for dropping files in the canvas
+ Nodes: {}, //node types by classname
+ Globals: {}, //used to store vars between graphs
+
+ searchbox_extras: {}, //used to add extra features to the search box
+ auto_sort_node_types: false, // [true!] If set to true, will automatically sort node types / categories in the context menus
+
+ node_box_coloured_when_on: false, // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback
+ node_box_coloured_by_mode: false, // [true!] nodebox based on node mode, visual feedback
+
+ dialog_close_on_mouse_leave: false, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
+ dialog_close_on_mouse_leave_delay: 500,
+
+ shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys
+ click_do_break_link_to: false, // [false!]prefer false, way too easy to break links
+
+ search_hide_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
+ search_filter_enabled: false, // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out]
+ search_show_all_on_open: true, // [true!] opens the results list when opening the search widget
+
+ auto_load_slot_types: false, // [if want false, use true, run, get vars values to be statically set, than disable] nodes types and nodeclass association with node types need to be calculated, if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out]
+
+ // set these values if not using auto_load_slot_types
+ registered_slot_in_types: {}, // slot types for nodeclass
+ registered_slot_out_types: {}, // slot types for nodeclass
+ slot_types_in: [], // slot types IN
+ slot_types_out: [], // slot types OUT
+ slot_types_default_in: [], // specify for each IN slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search
+ slot_types_default_out: [], // specify for each OUT slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search
+
+ alt_drag_do_clone_nodes: false, // [true!] very handy, ALT click to clone and drag the new node
+
+ do_add_triggers_slots: false, // [true!] will create and connect event slots when using action/events connections, !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this
+
+ allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentially, one by one
+
+ middle_click_slot_add_default_node: false, //[true!] allows to create and connect a ndoe clicking with the third button (wheel)
+
+ release_link_on_empty_shows_menu: false, //[true!] dragging a link to empty space will open a menu, add from list, search or defaults
+
+ pointerevents_method: "pointer", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now)
+ // TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary)
+
+ ctrl_shift_v_paste_connect_unselected_outputs: true, //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes
+
+ // if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers.
+ // use this if you must have node IDs that are unique across all graphs and subgraphs.
+ use_uuids: false,
+
+ /**
+ * Register a node class so it can be listed when the user wants to create a new one
+ * @method registerNodeType
+ * @param {String} type name of the node and path
+ * @param {Class} base_class class containing the structure of a node
+ */
+
+ registerNodeType: function(type, base_class) {
+ if (!base_class.prototype) {
+ throw "Cannot register a simple object, it must be a class with a prototype";
+ }
+ base_class.type = type;
+
+ if (LiteGraph.debug) {
+ console.log("Node registered: " + type);
+ }
+
+ const classname = base_class.name;
+
+ const pos = type.lastIndexOf("/");
+ base_class.category = type.substring(0, pos);
+
+ if (!base_class.title) {
+ base_class.title = classname;
+ }
+
+ //extend class
+ for (var i in LGraphNode.prototype) {
+ if (!base_class.prototype[i]) {
+ base_class.prototype[i] = LGraphNode.prototype[i];
+ }
+ }
+
+ const prev = this.registered_node_types[type];
+ if(prev) {
+ console.log("replacing node type: " + type);
+ }
+ if( !Object.prototype.hasOwnProperty.call( base_class.prototype, "shape") ) {
+ Object.defineProperty(base_class.prototype, "shape", {
+ set: function(v) {
+ switch (v) {
+ case "default":
+ delete this._shape;
+ break;
+ case "box":
+ this._shape = LiteGraph.BOX_SHAPE;
+ break;
+ case "round":
+ this._shape = LiteGraph.ROUND_SHAPE;
+ break;
+ case "circle":
+ this._shape = LiteGraph.CIRCLE_SHAPE;
+ break;
+ case "card":
+ this._shape = LiteGraph.CARD_SHAPE;
+ break;
+ default:
+ this._shape = v;
+ }
+ },
+ get: function() {
+ return this._shape;
+ },
+ enumerable: true,
+ configurable: true
+ });
+
+
+ //used to know which nodes to create when dragging files to the canvas
+ if (base_class.supported_extensions) {
+ for (let i in base_class.supported_extensions) {
+ const ext = base_class.supported_extensions[i];
+ if(ext && ext.constructor === String) {
+ this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class;
+ }
+ }
+ }
+ }
+
+ this.registered_node_types[type] = base_class;
+ if (base_class.constructor.name) {
+ this.Nodes[classname] = base_class;
+ }
+ if (LiteGraph.onNodeTypeRegistered) {
+ LiteGraph.onNodeTypeRegistered(type, base_class);
+ }
+ if (prev && LiteGraph.onNodeTypeReplaced) {
+ LiteGraph.onNodeTypeReplaced(type, base_class, prev);
+ }
+
+ //warnings
+ if (base_class.prototype.onPropertyChange) {
+ console.warn(
+ "LiteGraph node class " +
+ type +
+ " has onPropertyChange method, it must be called onPropertyChanged with d at the end"
+ );
+ }
+
+ // TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
+ if (this.auto_load_slot_types) {
+ new base_class(base_class.title || "tmpnode");
+ }
+ },
+
+ /**
+ * removes a node type from the system
+ * @method unregisterNodeType
+ * @param {String|Object} type name of the node or the node constructor itself
+ */
+ unregisterNodeType: function(type) {
+ const base_class =
+ type.constructor === String
+ ? this.registered_node_types[type]
+ : type;
+ if (!base_class) {
+ throw "node type not found: " + type;
+ }
+ delete this.registered_node_types[base_class.type];
+ if (base_class.constructor.name) {
+ delete this.Nodes[base_class.constructor.name];
+ }
+ },
+
+ /**
+ * Save a slot type and his node
+ * @method registerSlotType
+ * @param {String|Object} type name of the node or the node constructor itself
+ * @param {String} slot_type name of the slot type (variable type), eg. string, number, array, boolean, ..
+ */
+ registerNodeAndSlotType: function(type, slot_type, out){
+ out = out || false;
+ const base_class =
+ type.constructor === String &&
+ this.registered_node_types[type] !== "anonymous"
+ ? this.registered_node_types[type]
+ : type;
+
+ const class_type = base_class.constructor.type;
+
+ let allTypes = [];
+ if (typeof slot_type === "string") {
+ allTypes = slot_type.split(",");
+ } else if (slot_type == this.EVENT || slot_type == this.ACTION) {
+ allTypes = ["_event_"];
+ } else {
+ allTypes = ["*"];
+ }
+
+ for (let i = 0; i < allTypes.length; ++i) {
+ let slotType = allTypes[i];
+ if (slotType === "") {
+ slotType = "*";
+ }
+ const registerTo = out
+ ? "registered_slot_out_types"
+ : "registered_slot_in_types";
+ if (this[registerTo][slotType] === undefined) {
+ this[registerTo][slotType] = { nodes: [] };
+ }
+ if (!this[registerTo][slotType].nodes.includes(class_type)) {
+ this[registerTo][slotType].nodes.push(class_type);
+ }
+
+ // check if is a new type
+ if (!out) {
+ if (!this.slot_types_in.includes(slotType.toLowerCase())) {
+ this.slot_types_in.push(slotType.toLowerCase());
+ this.slot_types_in.sort();
+ }
+ } else {
+ if (!this.slot_types_out.includes(slotType.toLowerCase())) {
+ this.slot_types_out.push(slotType.toLowerCase());
+ this.slot_types_out.sort();
+ }
+ }
+ }
+ },
+
+ /**
+ * Create a new nodetype by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function.
+ * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output.
+ * @method wrapFunctionAsNode
+ * @param {String} name node name with namespace (p.e.: 'math/sum')
+ * @param {Function} func
+ * @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type
+ * @param {String} return_type [optional] string with the return type, otherwise it will be generic
+ * @param {Object} properties [optional] properties to be configurable
+ */
+ wrapFunctionAsNode: function(
+ name,
+ func,
+ param_types,
+ return_type,
+ properties
+ ) {
+ var params = Array(func.length);
+ var code = "";
+ var names = LiteGraph.getParameterNames(func);
+ for (var i = 0; i < names.length; ++i) {
+ code +=
+ "this.addInput('" +
+ names[i] +
+ "'," +
+ (param_types && param_types[i]
+ ? "'" + param_types[i] + "'"
+ : "0") +
+ ");\n";
+ }
+ code +=
+ "this.addOutput('out'," +
+ (return_type ? "'" + return_type + "'" : 0) +
+ ");\n";
+ if (properties) {
+ code +=
+ "this.properties = " + JSON.stringify(properties) + ";\n";
+ }
+ var classobj = Function(code);
+ classobj.title = name.split("/").pop();
+ classobj.desc = "Generated from " + func.name;
+ classobj.prototype.onExecute = function onExecute() {
+ for (var i = 0; i < params.length; ++i) {
+ params[i] = this.getInputData(i);
+ }
+ var r = func.apply(this, params);
+ this.setOutputData(0, r);
+ };
+ this.registerNodeType(name, classobj);
+ },
+
+ /**
+ * Removes all previously registered node's types
+ */
+ clearRegisteredTypes: function() {
+ this.registered_node_types = {};
+ this.node_types_by_file_extension = {};
+ this.Nodes = {};
+ this.searchbox_extras = {};
+ },
+
+ /**
+ * Adds this method to all nodetypes, existing and to be created
+ * (You can add it to LGraphNode.prototype but then existing node types wont have it)
+ * @method addNodeMethod
+ * @param {Function} func
+ */
+ addNodeMethod: function(name, func) {
+ LGraphNode.prototype[name] = func;
+ for (var i in this.registered_node_types) {
+ var type = this.registered_node_types[i];
+ if (type.prototype[name]) {
+ type.prototype["_" + name] = type.prototype[name];
+ } //keep old in case of replacing
+ type.prototype[name] = func;
+ }
+ },
+
+ /**
+ * Create a node of a given type with a name. The node is not attached to any graph yet.
+ * @method createNode
+ * @param {String} type full name of the node class. p.e. "math/sin"
+ * @param {String} name a name to distinguish from other nodes
+ * @param {Object} options to set options
+ */
+
+ createNode: function(type, title, options) {
+ var base_class = this.registered_node_types[type];
+ if (!base_class) {
+ if (LiteGraph.debug) {
+ console.log(
+ 'GraphNode type "' + type + '" not registered.'
+ );
+ }
+ return null;
+ }
+
+ var prototype = base_class.prototype || base_class;
+
+ title = title || base_class.title || type;
+
+ var node = null;
+
+ if (LiteGraph.catch_exceptions) {
+ try {
+ node = new base_class(title);
+ } catch (err) {
+ console.error(err);
+ return null;
+ }
+ } else {
+ node = new base_class(title);
+ }
+
+ node.type = type;
+
+ if (!node.title && title) {
+ node.title = title;
+ }
+ if (!node.properties) {
+ node.properties = {};
+ }
+ if (!node.properties_info) {
+ node.properties_info = [];
+ }
+ if (!node.flags) {
+ node.flags = {};
+ }
+ if (!node.size) {
+ node.size = node.computeSize();
+ //call onresize?
+ }
+ if (!node.pos) {
+ node.pos = LiteGraph.DEFAULT_POSITION.concat();
+ }
+ if (!node.mode) {
+ node.mode = LiteGraph.ALWAYS;
+ }
+
+ //extra options
+ if (options) {
+ for (var i in options) {
+ node[i] = options[i];
+ }
+ }
+
+ // callback
+ if ( node.onNodeCreated ) {
+ node.onNodeCreated();
+ }
+
+ return node;
+ },
+
+ /**
+ * Returns a registered node type with a given name
+ * @method getNodeType
+ * @param {String} type full name of the node class. p.e. "math/sin"
+ * @return {Class} the node class
+ */
+ getNodeType: function(type) {
+ return this.registered_node_types[type];
+ },
+
+ /**
+ * Returns a list of node types matching one category
+ * @method getNodeType
+ * @param {String} category category name
+ * @return {Array} array with all the node classes
+ */
+
+ getNodeTypesInCategory: function(category, filter) {
+ var r = [];
+ for (var i in this.registered_node_types) {
+ var type = this.registered_node_types[i];
+ if (type.filter != filter) {
+ continue;
+ }
+
+ if (category == "") {
+ if (type.category == null) {
+ r.push(type);
+ }
+ } else if (type.category == category) {
+ r.push(type);
+ }
+ }
+
+ if (this.auto_sort_node_types) {
+ r.sort(function(a,b){return a.title.localeCompare(b.title)});
+ }
+
+ return r;
+ },
+
+ /**
+ * Returns a list with all the node type categories
+ * @method getNodeTypesCategories
+ * @param {String} filter only nodes with ctor.filter equal can be shown
+ * @return {Array} array with all the names of the categories
+ */
+ getNodeTypesCategories: function( filter ) {
+ var categories = { "": 1 };
+ for (var i in this.registered_node_types) {
+ var type = this.registered_node_types[i];
+ if ( type.category && !type.skip_list )
+ {
+ if(type.filter != filter)
+ continue;
+ categories[type.category] = 1;
+ }
+ }
+ var result = [];
+ for (var i in categories) {
+ result.push(i);
+ }
+ return this.auto_sort_node_types ? result.sort() : result;
+ },
+
+ //debug purposes: reloads all the js scripts that matches a wildcard
+ reloadNodes: function(folder_wildcard) {
+ var tmp = document.getElementsByTagName("script");
+ //weird, this array changes by its own, so we use a copy
+ var script_files = [];
+ for (var i=0; i < tmp.length; i++) {
+ script_files.push(tmp[i]);
+ }
+
+ var docHeadObj = document.getElementsByTagName("head")[0];
+ folder_wildcard = document.location.href + folder_wildcard;
+
+ for (var i=0; i < script_files.length; i++) {
+ var src = script_files[i].src;
+ if (
+ !src ||
+ src.substr(0, folder_wildcard.length) != folder_wildcard
+ ) {
+ continue;
+ }
+
+ try {
+ if (LiteGraph.debug) {
+ console.log("Reloading: " + src);
+ }
+ var dynamicScript = document.createElement("script");
+ dynamicScript.type = "text/javascript";
+ dynamicScript.src = src;
+ docHeadObj.appendChild(dynamicScript);
+ docHeadObj.removeChild(script_files[i]);
+ } catch (err) {
+ if (LiteGraph.throw_errors) {
+ throw err;
+ }
+ if (LiteGraph.debug) {
+ console.log("Error while reloading " + src);
+ }
+ }
+ }
+
+ if (LiteGraph.debug) {
+ console.log("Nodes reloaded");
+ }
+ },
+
+ //separated just to improve if it doesn't work
+ cloneObject: function(obj, target) {
+ if (obj == null) {
+ return null;
+ }
+ var r = JSON.parse(JSON.stringify(obj));
+ if (!target) {
+ return r;
+ }
+
+ for (var i in r) {
+ target[i] = r[i];
+ }
+ return target;
+ },
+
+ /*
+ * https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670
+ */
+ uuidv4: function() {
+ return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,a=>(a^Math.random()*16>>a/4).toString(16));
+ },
+
+ /**
+ * Returns if the types of two slots are compatible (taking into account wildcards, etc)
+ * @method isValidConnection
+ * @param {String} type_a
+ * @param {String} type_b
+ * @return {Boolean} true if they can be connected
+ */
+ isValidConnection: function(type_a, type_b) {
+ if (type_a=="" || type_a==="*") type_a = 0;
+ if (type_b=="" || type_b==="*") type_b = 0;
+ if (
+ !type_a //generic output
+ || !type_b // generic input
+ || type_a == type_b //same type (is valid for triggers)
+ || (type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION)
+ ) {
+ return true;
+ }
+
+ // Enforce string type to handle toLowerCase call (-1 number not ok)
+ type_a = String(type_a);
+ type_b = String(type_b);
+ type_a = type_a.toLowerCase();
+ type_b = type_b.toLowerCase();
+
+ // For nodes supporting multiple connection types
+ if (type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1) {
+ return type_a == type_b;
+ }
+
+ // Check all permutations to see if one is valid
+ var supported_types_a = type_a.split(",");
+ var supported_types_b = type_b.split(",");
+ for (var i = 0; i < supported_types_a.length; ++i) {
+ for (var j = 0; j < supported_types_b.length; ++j) {
+ if(this.isValidConnection(supported_types_a[i],supported_types_b[j])){
+ //if (supported_types_a[i] == supported_types_b[j]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Register a string in the search box so when the user types it it will recommend this node
+ * @method registerSearchboxExtra
+ * @param {String} node_type the node recommended
+ * @param {String} description text to show next to it
+ * @param {Object} data it could contain info of how the node should be configured
+ * @return {Boolean} true if they can be connected
+ */
+ registerSearchboxExtra: function(node_type, description, data) {
+ this.searchbox_extras[description.toLowerCase()] = {
+ type: node_type,
+ desc: description,
+ data: data
+ };
+ },
+
+ /**
+ * Wrapper to load files (from url using fetch or from file using FileReader)
+ * @method fetchFile
+ * @param {String|File|Blob} url the url of the file (or the file itself)
+ * @param {String} type an string to know how to fetch it: "text","arraybuffer","json","blob"
+ * @param {Function} on_complete callback(data)
+ * @param {Function} on_error in case of an error
+ * @return {FileReader|Promise} returns the object used to
+ */
+ fetchFile: function( url, type, on_complete, on_error ) {
+ var that = this;
+ if(!url)
+ return null;
+
+ type = type || "text";
+ if( url.constructor === String )
+ {
+ if (url.substr(0, 4) == "http" && LiteGraph.proxy) {
+ url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3);
+ }
+ return fetch(url)
+ .then(function(response) {
+ if(!response.ok)
+ throw new Error("File not found"); //it will be catch below
+ if(type == "arraybuffer")
+ return response.arrayBuffer();
+ else if(type == "text" || type == "string")
+ return response.text();
+ else if(type == "json")
+ return response.json();
+ else if(type == "blob")
+ return response.blob();
+ })
+ .then(function(data) {
+ if(on_complete)
+ on_complete(data);
+ })
+ .catch(function(error) {
+ console.error("error fetching file:",url);
+ if(on_error)
+ on_error(error);
+ });
+ }
+ else if( url.constructor === File || url.constructor === Blob)
+ {
+ var reader = new FileReader();
+ reader.onload = function(e)
+ {
+ var v = e.target.result;
+ if( type == "json" )
+ v = JSON.parse(v);
+ if(on_complete)
+ on_complete(v);
+ }
+ if(type == "arraybuffer")
+ return reader.readAsArrayBuffer(url);
+ else if(type == "text" || type == "json")
+ return reader.readAsText(url);
+ else if(type == "blob")
+ return reader.readAsBinaryString(url);
+ }
+ return null;
+ }
+ });
+
+ //timer that works everywhere
+ if (typeof performance != "undefined") {
+ LiteGraph.getTime = performance.now.bind(performance);
+ } else if (typeof Date != "undefined" && Date.now) {
+ LiteGraph.getTime = Date.now.bind(Date);
+ } else if (typeof process != "undefined") {
+ LiteGraph.getTime = function() {
+ var t = process.hrtime();
+ return t[0] * 0.001 + t[1] * 1e-6;
+ };
+ } else {
+ LiteGraph.getTime = function getTime() {
+ return new Date().getTime();
+ };
+ }
+
+ //*********************************************************************************
+ // LGraph CLASS
+ //*********************************************************************************
+
+ /**
+ * LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop.
+ * supported callbacks:
+ + onNodeAdded: when a new node is added to the graph
+ + onNodeRemoved: when a node inside this graph is removed
+ + onNodeConnectionChange: some connection has changed in the graph (connected or disconnected)
+ *
+ * @class LGraph
+ * @constructor
+ * @param {Object} o data from previous serialization [optional]
+ */
+
+ function LGraph(o) {
+ if (LiteGraph.debug) {
+ console.log("Graph created");
+ }
+ this.list_of_graphcanvas = null;
+ this.clear();
+
+ if (o) {
+ this.configure(o);
+ }
+ }
+
+ global.LGraph = LiteGraph.LGraph = LGraph;
+
+ //default supported types
+ LGraph.supported_types = ["number", "string", "boolean"];
+
+ //used to know which types of connections support this graph (some graphs do not allow certain types)
+ LGraph.prototype.getSupportedTypes = function() {
+ return this.supported_types || LGraph.supported_types;
+ };
+
+ LGraph.STATUS_STOPPED = 1;
+ LGraph.STATUS_RUNNING = 2;
+
+ /**
+ * Removes all nodes from this graph
+ * @method clear
+ */
+
+ LGraph.prototype.clear = function() {
+ this.stop();
+ this.status = LGraph.STATUS_STOPPED;
+
+ this.last_node_id = 0;
+ this.last_link_id = 0;
+
+ this._version = -1; //used to detect changes
+
+ //safe clear
+ if (this._nodes) {
+ for (var i = 0; i < this._nodes.length; ++i) {
+ var node = this._nodes[i];
+ if (node.onRemoved) {
+ node.onRemoved();
+ }
+ }
+ }
+
+ //nodes
+ this._nodes = [];
+ this._nodes_by_id = {};
+ this._nodes_in_order = []; //nodes sorted in execution order
+ this._nodes_executable = null; //nodes that contain onExecute sorted in execution order
+
+ //other scene stuff
+ this._groups = [];
+
+ //links
+ this.links = {}; //container with all the links
+
+ //iterations
+ this.iteration = 0;
+
+ //custom data
+ this.config = {};
+ this.vars = {};
+ this.extra = {}; //to store custom data
+
+ //timing
+ this.globaltime = 0;
+ this.runningtime = 0;
+ this.fixedtime = 0;
+ this.fixedtime_lapse = 0.01;
+ this.elapsed_time = 0.01;
+ this.last_update_time = 0;
+ this.starttime = 0;
+
+ this.catch_errors = true;
+
+ this.nodes_executing = [];
+ this.nodes_actioning = [];
+ this.nodes_executedAction = [];
+
+ //subgraph_data
+ this.inputs = {};
+ this.outputs = {};
+
+ //notify canvas to redraw
+ this.change();
+
+ this.sendActionToCanvas("clear");
+ };
+
+ /**
+ * Attach Canvas to this graph
+ * @method attachCanvas
+ * @param {GraphCanvas} graph_canvas
+ */
+
+ LGraph.prototype.attachCanvas = function(graphcanvas) {
+ if (graphcanvas.constructor != LGraphCanvas) {
+ throw "attachCanvas expects a LGraphCanvas instance";
+ }
+ if (graphcanvas.graph && graphcanvas.graph != this) {
+ graphcanvas.graph.detachCanvas(graphcanvas);
+ }
+
+ graphcanvas.graph = this;
+
+ if (!this.list_of_graphcanvas) {
+ this.list_of_graphcanvas = [];
+ }
+ this.list_of_graphcanvas.push(graphcanvas);
+ };
+
+ /**
+ * Detach Canvas from this graph
+ * @method detachCanvas
+ * @param {GraphCanvas} graph_canvas
+ */
+ LGraph.prototype.detachCanvas = function(graphcanvas) {
+ if (!this.list_of_graphcanvas) {
+ return;
+ }
+
+ var pos = this.list_of_graphcanvas.indexOf(graphcanvas);
+ if (pos == -1) {
+ return;
+ }
+ graphcanvas.graph = null;
+ this.list_of_graphcanvas.splice(pos, 1);
+ };
+
+ /**
+ * Starts running this graph every interval milliseconds.
+ * @method start
+ * @param {number} interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate
+ */
+
+ LGraph.prototype.start = function(interval) {
+ if (this.status == LGraph.STATUS_RUNNING) {
+ return;
+ }
+ this.status = LGraph.STATUS_RUNNING;
+
+ if (this.onPlayEvent) {
+ this.onPlayEvent();
+ }
+
+ this.sendEventToAllNodes("onStart");
+
+ //launch
+ this.starttime = LiteGraph.getTime();
+ this.last_update_time = this.starttime;
+ interval = interval || 0;
+ var that = this;
+
+ //execute once per frame
+ if ( interval == 0 && typeof window != "undefined" && window.requestAnimationFrame ) {
+ function on_frame() {
+ if (that.execution_timer_id != -1) {
+ return;
+ }
+ window.requestAnimationFrame(on_frame);
+ if(that.onBeforeStep)
+ that.onBeforeStep();
+ that.runStep(1, !that.catch_errors);
+ if(that.onAfterStep)
+ that.onAfterStep();
+ }
+ this.execution_timer_id = -1;
+ on_frame();
+ } else { //execute every 'interval' ms
+ this.execution_timer_id = setInterval(function() {
+ //execute
+ if(that.onBeforeStep)
+ that.onBeforeStep();
+ that.runStep(1, !that.catch_errors);
+ if(that.onAfterStep)
+ that.onAfterStep();
+ }, interval);
+ }
+ };
+
+ /**
+ * Stops the execution loop of the graph
+ * @method stop execution
+ */
+
+ LGraph.prototype.stop = function() {
+ if (this.status == LGraph.STATUS_STOPPED) {
+ return;
+ }
+
+ this.status = LGraph.STATUS_STOPPED;
+
+ if (this.onStopEvent) {
+ this.onStopEvent();
+ }
+
+ if (this.execution_timer_id != null) {
+ if (this.execution_timer_id != -1) {
+ clearInterval(this.execution_timer_id);
+ }
+ this.execution_timer_id = null;
+ }
+
+ this.sendEventToAllNodes("onStop");
+ };
+
+ /**
+ * Run N steps (cycles) of the graph
+ * @method runStep
+ * @param {number} num number of steps to run, default is 1
+ * @param {Boolean} do_not_catch_errors [optional] if you want to try/catch errors
+ * @param {number} limit max number of nodes to execute (used to execute from start to a node)
+ */
+
+ LGraph.prototype.runStep = function(num, do_not_catch_errors, limit ) {
+ num = num || 1;
+
+ var start = LiteGraph.getTime();
+ this.globaltime = 0.001 * (start - this.starttime);
+
+ var nodes = this._nodes_executable
+ ? this._nodes_executable
+ : this._nodes;
+ if (!nodes) {
+ return;
+ }
+
+ limit = limit || nodes.length;
+
+ if (do_not_catch_errors) {
+ //iterations
+ for (var i = 0; i < num; i++) {
+ for (var j = 0; j < limit; ++j) {
+ var node = nodes[j];
+ if (node.mode == LiteGraph.ALWAYS && node.onExecute) {
+ //wrap node.onExecute();
+ node.doExecute();
+ }
+ }
+
+ this.fixedtime += this.fixedtime_lapse;
+ if (this.onExecuteStep) {
+ this.onExecuteStep();
+ }
+ }
+
+ if (this.onAfterExecute) {
+ this.onAfterExecute();
+ }
+ } else {
+ try {
+ //iterations
+ for (var i = 0; i < num; i++) {
+ for (var j = 0; j < limit; ++j) {
+ var node = nodes[j];
+ if (node.mode == LiteGraph.ALWAYS && node.onExecute) {
+ node.onExecute();
+ }
+ }
+
+ this.fixedtime += this.fixedtime_lapse;
+ if (this.onExecuteStep) {
+ this.onExecuteStep();
+ }
+ }
+
+ if (this.onAfterExecute) {
+ this.onAfterExecute();
+ }
+ this.errors_in_execution = false;
+ } catch (err) {
+ this.errors_in_execution = true;
+ if (LiteGraph.throw_errors) {
+ throw err;
+ }
+ if (LiteGraph.debug) {
+ console.log("Error during execution: " + err);
+ }
+ this.stop();
+ }
+ }
+
+ var now = LiteGraph.getTime();
+ var elapsed = now - start;
+ if (elapsed == 0) {
+ elapsed = 1;
+ }
+ this.execution_time = 0.001 * elapsed;
+ this.globaltime += 0.001 * elapsed;
+ this.iteration += 1;
+ this.elapsed_time = (now - this.last_update_time) * 0.001;
+ this.last_update_time = now;
+ this.nodes_executing = [];
+ this.nodes_actioning = [];
+ this.nodes_executedAction = [];
+ };
+
+ /**
+ * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than
+ * nodes with only inputs.
+ * @method updateExecutionOrder
+ */
+ LGraph.prototype.updateExecutionOrder = function() {
+ this._nodes_in_order = this.computeExecutionOrder(false);
+ this._nodes_executable = [];
+ for (var i = 0; i < this._nodes_in_order.length; ++i) {
+ if (this._nodes_in_order[i].onExecute) {
+ this._nodes_executable.push(this._nodes_in_order[i]);
+ }
+ }
+ };
+
+ //This is more internal, it computes the executable nodes in order and returns it
+ LGraph.prototype.computeExecutionOrder = function(
+ only_onExecute,
+ set_level
+ ) {
+ var L = [];
+ var S = [];
+ var M = {};
+ var visited_links = {}; //to avoid repeating links
+ var remaining_links = {}; //to a
+
+ //search for the nodes without inputs (starting nodes)
+ for (var i = 0, l = this._nodes.length; i < l; ++i) {
+ var node = this._nodes[i];
+ if (only_onExecute && !node.onExecute) {
+ continue;
+ }
+
+ M[node.id] = node; //add to pending nodes
+
+ var num = 0; //num of input connections
+ if (node.inputs) {
+ for (var j = 0, l2 = node.inputs.length; j < l2; j++) {
+ if (node.inputs[j] && node.inputs[j].link != null) {
+ num += 1;
+ }
+ }
+ }
+
+ if (num == 0) {
+ //is a starting node
+ S.push(node);
+ if (set_level) {
+ node._level = 1;
+ }
+ } //num of input links
+ else {
+ if (set_level) {
+ node._level = 0;
+ }
+ remaining_links[node.id] = num;
+ }
+ }
+
+ while (true) {
+ if (S.length == 0) {
+ break;
+ }
+
+ //get an starting node
+ var node = S.shift();
+ L.push(node); //add to ordered list
+ delete M[node.id]; //remove from the pending nodes
+
+ if (!node.outputs) {
+ continue;
+ }
+
+ //for every output
+ for (var i = 0; i < node.outputs.length; i++) {
+ var output = node.outputs[i];
+ //not connected
+ if (
+ output == null ||
+ output.links == null ||
+ output.links.length == 0
+ ) {
+ continue;
+ }
+
+ //for every connection
+ for (var j = 0; j < output.links.length; j++) {
+ var link_id = output.links[j];
+ var link = this.links[link_id];
+ if (!link) {
+ continue;
+ }
+
+ //already visited link (ignore it)
+ if (visited_links[link.id]) {
+ continue;
+ }
+
+ var target_node = this.getNodeById(link.target_id);
+ if (target_node == null) {
+ visited_links[link.id] = true;
+ continue;
+ }
+
+ if (
+ set_level &&
+ (!target_node._level ||
+ target_node._level <= node._level)
+ ) {
+ target_node._level = node._level + 1;
+ }
+
+ visited_links[link.id] = true; //mark as visited
+ remaining_links[target_node.id] -= 1; //reduce the number of links remaining
+ if (remaining_links[target_node.id] == 0) {
+ S.push(target_node);
+ } //if no more links, then add to starters array
+ }
+ }
+ }
+
+ //the remaining ones (loops)
+ for (var i in M) {
+ L.push(M[i]);
+ }
+
+ if (L.length != this._nodes.length && LiteGraph.debug) {
+ console.warn("something went wrong, nodes missing");
+ }
+
+ var l = L.length;
+
+ //save order number in the node
+ for (var i = 0; i < l; ++i) {
+ L[i].order = i;
+ }
+
+ //sort now by priority
+ L = L.sort(function(A, B) {
+ var Ap = A.constructor.priority || A.priority || 0;
+ var Bp = B.constructor.priority || B.priority || 0;
+ if (Ap == Bp) {
+ //if same priority, sort by order
+ return A.order - B.order;
+ }
+ return Ap - Bp; //sort by priority
+ });
+
+ //save order number in the node, again...
+ for (var i = 0; i < l; ++i) {
+ L[i].order = i;
+ }
+
+ return L;
+ };
+
+ /**
+ * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively.
+ * It doesn't include the node itself
+ * @method getAncestors
+ * @return {Array} an array with all the LGraphNodes that affect this node, in order of execution
+ */
+ LGraph.prototype.getAncestors = function(node) {
+ var ancestors = [];
+ var pending = [node];
+ var visited = {};
+
+ while (pending.length) {
+ var current = pending.shift();
+ if (!current.inputs) {
+ continue;
+ }
+ if (!visited[current.id] && current != node) {
+ visited[current.id] = true;
+ ancestors.push(current);
+ }
+
+ for (var i = 0; i < current.inputs.length; ++i) {
+ var input = current.getInputNode(i);
+ if (input && ancestors.indexOf(input) == -1) {
+ pending.push(input);
+ }
+ }
+ }
+
+ ancestors.sort(function(a, b) {
+ return a.order - b.order;
+ });
+ return ancestors;
+ };
+
+ /**
+ * Positions every node in a more readable manner
+ * @method arrange
+ */
+ LGraph.prototype.arrange = function (margin, layout) {
+ margin = margin || 100;
+
+ const nodes = this.computeExecutionOrder(false, true);
+ const columns = [];
+ for (let i = 0; i < nodes.length; ++i) {
+ const node = nodes[i];
+ const col = node._level || 1;
+ if (!columns[col]) {
+ columns[col] = [];
+ }
+ columns[col].push(node);
+ }
+
+ let x = margin;
+
+ for (let i = 0; i < columns.length; ++i) {
+ const column = columns[i];
+ if (!column) {
+ continue;
+ }
+ let max_size = 100;
+ let y = margin + LiteGraph.NODE_TITLE_HEIGHT;
+ for (let j = 0; j < column.length; ++j) {
+ const node = column[j];
+ node.pos[0] = (layout == LiteGraph.VERTICAL_LAYOUT) ? y : x;
+ node.pos[1] = (layout == LiteGraph.VERTICAL_LAYOUT) ? x : y;
+ const max_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 1 : 0;
+ if (node.size[max_size_index] > max_size) {
+ max_size = node.size[max_size_index];
+ }
+ const node_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 0 : 1;
+ y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT;
+ }
+ x += max_size + margin;
+ }
+
+ this.setDirtyCanvas(true, true);
+ };
+
+ /**
+ * Returns the amount of time the graph has been running in milliseconds
+ * @method getTime
+ * @return {number} number of milliseconds the graph has been running
+ */
+ LGraph.prototype.getTime = function() {
+ return this.globaltime;
+ };
+
+ /**
+ * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant
+ * @method getFixedTime
+ * @return {number} number of milliseconds the graph has been running
+ */
+
+ LGraph.prototype.getFixedTime = function() {
+ return this.fixedtime;
+ };
+
+ /**
+ * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct
+ * if the nodes are using graphical actions
+ * @method getElapsedTime
+ * @return {number} number of milliseconds it took the last cycle
+ */
+
+ LGraph.prototype.getElapsedTime = function() {
+ return this.elapsed_time;
+ };
+
+ /**
+ * Sends an event to all the nodes, useful to trigger stuff
+ * @method sendEventToAllNodes
+ * @param {String} eventname the name of the event (function to be called)
+ * @param {Array} params parameters in array format
+ */
+ LGraph.prototype.sendEventToAllNodes = function(eventname, params, mode) {
+ mode = mode || LiteGraph.ALWAYS;
+
+ var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes;
+ if (!nodes) {
+ return;
+ }
+
+ for (var j = 0, l = nodes.length; j < l; ++j) {
+ var node = nodes[j];
+
+ if (
+ node.constructor === LiteGraph.Subgraph &&
+ eventname != "onExecute"
+ ) {
+ if (node.mode == mode) {
+ node.sendEventToAllNodes(eventname, params, mode);
+ }
+ continue;
+ }
+
+ if (!node[eventname] || node.mode != mode) {
+ continue;
+ }
+ if (params === undefined) {
+ node[eventname]();
+ } else if (params && params.constructor === Array) {
+ node[eventname].apply(node, params);
+ } else {
+ node[eventname](params);
+ }
+ }
+ };
+
+ LGraph.prototype.sendActionToCanvas = function(action, params) {
+ if (!this.list_of_graphcanvas) {
+ return;
+ }
+
+ for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
+ var c = this.list_of_graphcanvas[i];
+ if (c[action]) {
+ c[action].apply(c, params);
+ }
+ }
+ };
+
+ /**
+ * Adds a new node instance to this graph
+ * @method add
+ * @param {LGraphNode} node the instance of the node
+ */
+
+ LGraph.prototype.add = function(node, skip_compute_order) {
+ if (!node) {
+ return;
+ }
+
+ //groups
+ if (node.constructor === LGraphGroup) {
+ this._groups.push(node);
+ this.setDirtyCanvas(true);
+ this.change();
+ node.graph = this;
+ this._version++;
+ return;
+ }
+
+ //nodes
+ if (node.id != -1 && this._nodes_by_id[node.id] != null) {
+ console.warn(
+ "LiteGraph: there is already a node with this ID, changing it"
+ );
+ if (LiteGraph.use_uuids) {
+ node.id = LiteGraph.uuidv4();
+ }
+ else {
+ node.id = ++this.last_node_id;
+ }
+ }
+
+ if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
+ throw "LiteGraph: max number of nodes in a graph reached";
+ }
+
+ //give him an id
+ if (LiteGraph.use_uuids) {
+ if (node.id == null || node.id == -1)
+ node.id = LiteGraph.uuidv4();
+ }
+ else {
+ if (node.id == null || node.id == -1) {
+ node.id = ++this.last_node_id;
+ } else if (this.last_node_id < node.id) {
+ this.last_node_id = node.id;
+ }
+ }
+
+ node.graph = this;
+ this._version++;
+
+ this._nodes.push(node);
+ this._nodes_by_id[node.id] = node;
+
+ if (node.onAdded) {
+ node.onAdded(this);
+ }
+
+ if (this.config.align_to_grid) {
+ node.alignToGrid();
+ }
+
+ if (!skip_compute_order) {
+ this.updateExecutionOrder();
+ }
+
+ if (this.onNodeAdded) {
+ this.onNodeAdded(node);
+ }
+
+ this.setDirtyCanvas(true);
+ this.change();
+
+ return node; //to chain actions
+ };
+
+ /**
+ * Removes a node from the graph
+ * @method remove
+ * @param {LGraphNode} node the instance of the node
+ */
+
+ LGraph.prototype.remove = function(node) {
+ if (node.constructor === LiteGraph.LGraphGroup) {
+ var index = this._groups.indexOf(node);
+ if (index != -1) {
+ this._groups.splice(index, 1);
+ }
+ node.graph = null;
+ this._version++;
+ this.setDirtyCanvas(true, true);
+ this.change();
+ return;
+ }
+
+ if (this._nodes_by_id[node.id] == null) {
+ return;
+ } //not found
+
+ if (node.ignore_remove) {
+ return;
+ } //cannot be removed
+
+ this.beforeChange(); //sure? - almost sure is wrong
+
+ //disconnect inputs
+ if (node.inputs) {
+ for (var i = 0; i < node.inputs.length; i++) {
+ var slot = node.inputs[i];
+ if (slot.link != null) {
+ node.disconnectInput(i);
+ }
+ }
+ }
+
+ //disconnect outputs
+ if (node.outputs) {
+ for (var i = 0; i < node.outputs.length; i++) {
+ var slot = node.outputs[i];
+ if (slot.links != null && slot.links.length) {
+ node.disconnectOutput(i);
+ }
+ }
+ }
+
+ //node.id = -1; //why?
+
+ //callback
+ if (node.onRemoved) {
+ node.onRemoved();
+ }
+
+ node.graph = null;
+ this._version++;
+
+ //remove from canvas render
+ if (this.list_of_graphcanvas) {
+ for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
+ var canvas = this.list_of_graphcanvas[i];
+ if (canvas.selected_nodes[node.id]) {
+ delete canvas.selected_nodes[node.id];
+ }
+ if (canvas.node_dragged == node) {
+ canvas.node_dragged = null;
+ }
+ }
+ }
+
+ //remove from containers
+ var pos = this._nodes.indexOf(node);
+ if (pos != -1) {
+ this._nodes.splice(pos, 1);
+ }
+ delete this._nodes_by_id[node.id];
+
+ if (this.onNodeRemoved) {
+ this.onNodeRemoved(node);
+ }
+
+ //close panels
+ this.sendActionToCanvas("checkPanels");
+
+ this.setDirtyCanvas(true, true);
+ this.afterChange(); //sure? - almost sure is wrong
+ this.change();
+
+ this.updateExecutionOrder();
+ };
+
+ /**
+ * Returns a node by its id.
+ * @method getNodeById
+ * @param {Number} id
+ */
+
+ LGraph.prototype.getNodeById = function(id) {
+ if (id == null) {
+ return null;
+ }
+ return this._nodes_by_id[id];
+ };
+
+ /**
+ * Returns a list of nodes that matches a class
+ * @method findNodesByClass
+ * @param {Class} classObject the class itself (not an string)
+ * @return {Array} a list with all the nodes of this type
+ */
+ LGraph.prototype.findNodesByClass = function(classObject, result) {
+ result = result || [];
+ result.length = 0;
+ for (var i = 0, l = this._nodes.length; i < l; ++i) {
+ if (this._nodes[i].constructor === classObject) {
+ result.push(this._nodes[i]);
+ }
+ }
+ return result;
+ };
+
+ /**
+ * Returns a list of nodes that matches a type
+ * @method findNodesByType
+ * @param {String} type the name of the node type
+ * @return {Array} a list with all the nodes of this type
+ */
+ LGraph.prototype.findNodesByType = function(type, result) {
+ var type = type.toLowerCase();
+ result = result || [];
+ result.length = 0;
+ for (var i = 0, l = this._nodes.length; i < l; ++i) {
+ if (this._nodes[i].type.toLowerCase() == type) {
+ result.push(this._nodes[i]);
+ }
+ }
+ return result;
+ };
+
+ /**
+ * Returns the first node that matches a name in its title
+ * @method findNodeByTitle
+ * @param {String} name the name of the node to search
+ * @return {Node} the node or null
+ */
+ LGraph.prototype.findNodeByTitle = function(title) {
+ for (var i = 0, l = this._nodes.length; i < l; ++i) {
+ if (this._nodes[i].title == title) {
+ return this._nodes[i];
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Returns a list of nodes that matches a name
+ * @method findNodesByTitle
+ * @param {String} name the name of the node to search
+ * @return {Array} a list with all the nodes with this name
+ */
+ LGraph.prototype.findNodesByTitle = function(title) {
+ var result = [];
+ for (var i = 0, l = this._nodes.length; i < l; ++i) {
+ if (this._nodes[i].title == title) {
+ result.push(this._nodes[i]);
+ }
+ }
+ return result;
+ };
+
+ /**
+ * Returns the top-most node in this position of the canvas
+ * @method getNodeOnPos
+ * @param {number} x the x coordinate in canvas space
+ * @param {number} y the y coordinate in canvas space
+ * @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph
+ * @return {LGraphNode} the node at this position or null
+ */
+ LGraph.prototype.getNodeOnPos = function(x, y, nodes_list, margin) {
+ nodes_list = nodes_list || this._nodes;
+ var nRet = null;
+ for (var i = nodes_list.length - 1; i >= 0; i--) {
+ var n = nodes_list[i];
+ var skip_title = n.constructor.title_mode == LiteGraph.NO_TITLE;
+ if (n.isPointInside(x, y, margin, skip_title)) {
+ // check for lesser interest nodes (TODO check for overlapping, use the top)
+ /*if (typeof n == "LGraphGroup"){
+ nRet = n;
+ }else{*/
+ return n;
+ /*}*/
+ }
+ }
+ return nRet;
+ };
+
+ /**
+ * Returns the top-most group in that position
+ * @method getGroupOnPos
+ * @param {number} x the x coordinate in canvas space
+ * @param {number} y the y coordinate in canvas space
+ * @return {LGraphGroup} the group or null
+ */
+ LGraph.prototype.getGroupOnPos = function(x, y) {
+ for (var i = this._groups.length - 1; i >= 0; i--) {
+ var g = this._groups[i];
+ if (g.isPointInside(x, y, 2, true)) {
+ return g;
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Checks that the node type matches the node type registered, used when replacing a nodetype by a newer version during execution
+ * this replaces the ones using the old version with the new version
+ * @method checkNodeTypes
+ */
+ LGraph.prototype.checkNodeTypes = function() {
+ var changes = false;
+ for (var i = 0; i < this._nodes.length; i++) {
+ var node = this._nodes[i];
+ var ctor = LiteGraph.registered_node_types[node.type];
+ if (node.constructor == ctor) {
+ continue;
+ }
+ console.log("node being replaced by newer version: " + node.type);
+ var newnode = LiteGraph.createNode(node.type);
+ changes = true;
+ this._nodes[i] = newnode;
+ newnode.configure(node.serialize());
+ newnode.graph = this;
+ this._nodes_by_id[newnode.id] = newnode;
+ if (node.inputs) {
+ newnode.inputs = node.inputs.concat();
+ }
+ if (node.outputs) {
+ newnode.outputs = node.outputs.concat();
+ }
+ }
+ this.updateExecutionOrder();
+ };
+
+ // ********** GLOBALS *****************
+
+ LGraph.prototype.onAction = function(action, param, options) {
+ this._input_nodes = this.findNodesByClass(
+ LiteGraph.GraphInput,
+ this._input_nodes
+ );
+ for (var i = 0; i < this._input_nodes.length; ++i) {
+ var node = this._input_nodes[i];
+ if (node.properties.name != action) {
+ continue;
+ }
+ //wrap node.onAction(action, param);
+ node.actionDo(action, param, options);
+ break;
+ }
+ };
+
+ LGraph.prototype.trigger = function(action, param) {
+ if (this.onTrigger) {
+ this.onTrigger(action, param);
+ }
+ };
+
+ /**
+ * Tell this graph it has a global graph input of this type
+ * @method addGlobalInput
+ * @param {String} name
+ * @param {String} type
+ * @param {*} value [optional]
+ */
+ LGraph.prototype.addInput = function(name, type, value) {
+ var input = this.inputs[name];
+ if (input) {
+ //already exist
+ return;
+ }
+
+ this.beforeChange();
+ this.inputs[name] = { name: name, type: type, value: value };
+ this._version++;
+ this.afterChange();
+
+ if (this.onInputAdded) {
+ this.onInputAdded(name, type);
+ }
+
+ if (this.onInputsOutputsChange) {
+ this.onInputsOutputsChange();
+ }
+ };
+
+ /**
+ * Assign a data to the global graph input
+ * @method setGlobalInputData
+ * @param {String} name
+ * @param {*} data
+ */
+ LGraph.prototype.setInputData = function(name, data) {
+ var input = this.inputs[name];
+ if (!input) {
+ return;
+ }
+ input.value = data;
+ };
+
+ /**
+ * Returns the current value of a global graph input
+ * @method getInputData
+ * @param {String} name
+ * @return {*} the data
+ */
+ LGraph.prototype.getInputData = function(name) {
+ var input = this.inputs[name];
+ if (!input) {
+ return null;
+ }
+ return input.value;
+ };
+
+ /**
+ * Changes the name of a global graph input
+ * @method renameInput
+ * @param {String} old_name
+ * @param {String} new_name
+ */
+ LGraph.prototype.renameInput = function(old_name, name) {
+ if (name == old_name) {
+ return;
+ }
+
+ if (!this.inputs[old_name]) {
+ return false;
+ }
+
+ if (this.inputs[name]) {
+ console.error("there is already one input with that name");
+ return false;
+ }
+
+ this.inputs[name] = this.inputs[old_name];
+ delete this.inputs[old_name];
+ this._version++;
+
+ if (this.onInputRenamed) {
+ this.onInputRenamed(old_name, name);
+ }
+
+ if (this.onInputsOutputsChange) {
+ this.onInputsOutputsChange();
+ }
+ };
+
+ /**
+ * Changes the type of a global graph input
+ * @method changeInputType
+ * @param {String} name
+ * @param {String} type
+ */
+ LGraph.prototype.changeInputType = function(name, type) {
+ if (!this.inputs[name]) {
+ return false;
+ }
+
+ if (
+ this.inputs[name].type &&
+ String(this.inputs[name].type).toLowerCase() ==
+ String(type).toLowerCase()
+ ) {
+ return;
+ }
+
+ this.inputs[name].type = type;
+ this._version++;
+ if (this.onInputTypeChanged) {
+ this.onInputTypeChanged(name, type);
+ }
+ };
+
+ /**
+ * Removes a global graph input
+ * @method removeInput
+ * @param {String} name
+ * @param {String} type
+ */
+ LGraph.prototype.removeInput = function(name) {
+ if (!this.inputs[name]) {
+ return false;
+ }
+
+ delete this.inputs[name];
+ this._version++;
+
+ if (this.onInputRemoved) {
+ this.onInputRemoved(name);
+ }
+
+ if (this.onInputsOutputsChange) {
+ this.onInputsOutputsChange();
+ }
+ return true;
+ };
+
+ /**
+ * Creates a global graph output
+ * @method addOutput
+ * @param {String} name
+ * @param {String} type
+ * @param {*} value
+ */
+ LGraph.prototype.addOutput = function(name, type, value) {
+ this.outputs[name] = { name: name, type: type, value: value };
+ this._version++;
+
+ if (this.onOutputAdded) {
+ this.onOutputAdded(name, type);
+ }
+
+ if (this.onInputsOutputsChange) {
+ this.onInputsOutputsChange();
+ }
+ };
+
+ /**
+ * Assign a data to the global output
+ * @method setOutputData
+ * @param {String} name
+ * @param {String} value
+ */
+ LGraph.prototype.setOutputData = function(name, value) {
+ var output = this.outputs[name];
+ if (!output) {
+ return;
+ }
+ output.value = value;
+ };
+
+ /**
+ * Returns the current value of a global graph output
+ * @method getOutputData
+ * @param {String} name
+ * @return {*} the data
+ */
+ LGraph.prototype.getOutputData = function(name) {
+ var output = this.outputs[name];
+ if (!output) {
+ return null;
+ }
+ return output.value;
+ };
+
+ /**
+ * Renames a global graph output
+ * @method renameOutput
+ * @param {String} old_name
+ * @param {String} new_name
+ */
+ LGraph.prototype.renameOutput = function(old_name, name) {
+ if (!this.outputs[old_name]) {
+ return false;
+ }
+
+ if (this.outputs[name]) {
+ console.error("there is already one output with that name");
+ return false;
+ }
+
+ this.outputs[name] = this.outputs[old_name];
+ delete this.outputs[old_name];
+ this._version++;
+
+ if (this.onOutputRenamed) {
+ this.onOutputRenamed(old_name, name);
+ }
+
+ if (this.onInputsOutputsChange) {
+ this.onInputsOutputsChange();
+ }
+ };
+
+ /**
+ * Changes the type of a global graph output
+ * @method changeOutputType
+ * @param {String} name
+ * @param {String} type
+ */
+ LGraph.prototype.changeOutputType = function(name, type) {
+ if (!this.outputs[name]) {
+ return false;
+ }
+
+ if (
+ this.outputs[name].type &&
+ String(this.outputs[name].type).toLowerCase() ==
+ String(type).toLowerCase()
+ ) {
+ return;
+ }
+
+ this.outputs[name].type = type;
+ this._version++;
+ if (this.onOutputTypeChanged) {
+ this.onOutputTypeChanged(name, type);
+ }
+ };
+
+ /**
+ * Removes a global graph output
+ * @method removeOutput
+ * @param {String} name
+ */
+ LGraph.prototype.removeOutput = function(name) {
+ if (!this.outputs[name]) {
+ return false;
+ }
+ delete this.outputs[name];
+ this._version++;
+
+ if (this.onOutputRemoved) {
+ this.onOutputRemoved(name);
+ }
+
+ if (this.onInputsOutputsChange) {
+ this.onInputsOutputsChange();
+ }
+ return true;
+ };
+
+ LGraph.prototype.triggerInput = function(name, value) {
+ var nodes = this.findNodesByTitle(name);
+ for (var i = 0; i < nodes.length; ++i) {
+ nodes[i].onTrigger(value);
+ }
+ };
+
+ LGraph.prototype.setCallback = function(name, func) {
+ var nodes = this.findNodesByTitle(name);
+ for (var i = 0; i < nodes.length; ++i) {
+ nodes[i].setTrigger(func);
+ }
+ };
+
+ //used for undo, called before any change is made to the graph
+ LGraph.prototype.beforeChange = function(info) {
+ if (this.onBeforeChange) {
+ this.onBeforeChange(this,info);
+ }
+ this.sendActionToCanvas("onBeforeChange", this);
+ };
+
+ //used to resend actions, called after any change is made to the graph
+ LGraph.prototype.afterChange = function(info) {
+ if (this.onAfterChange) {
+ this.onAfterChange(this,info);
+ }
+ this.sendActionToCanvas("onAfterChange", this);
+ };
+
+ LGraph.prototype.connectionChange = function(node, link_info) {
+ this.updateExecutionOrder();
+ if (this.onConnectionChange) {
+ this.onConnectionChange(node);
+ }
+ this._version++;
+ this.sendActionToCanvas("onConnectionChange");
+ };
+
+ /**
+ * returns if the graph is in live mode
+ * @method isLive
+ */
+
+ LGraph.prototype.isLive = function() {
+ if (!this.list_of_graphcanvas) {
+ return false;
+ }
+
+ for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
+ var c = this.list_of_graphcanvas[i];
+ if (c.live_mode) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ /**
+ * clears the triggered slot animation in all links (stop visual animation)
+ * @method clearTriggeredSlots
+ */
+ LGraph.prototype.clearTriggeredSlots = function() {
+ for (var i in this.links) {
+ var link_info = this.links[i];
+ if (!link_info) {
+ continue;
+ }
+ if (link_info._last_time) {
+ link_info._last_time = 0;
+ }
+ }
+ };
+
+ /* Called when something visually changed (not the graph!) */
+ LGraph.prototype.change = function() {
+ if (LiteGraph.debug) {
+ console.log("Graph changed");
+ }
+ this.sendActionToCanvas("setDirty", [true, true]);
+ if (this.on_change) {
+ this.on_change(this);
+ }
+ };
+
+ LGraph.prototype.setDirtyCanvas = function(fg, bg) {
+ this.sendActionToCanvas("setDirty", [fg, bg]);
+ };
+
+ /**
+ * Destroys a link
+ * @method removeLink
+ * @param {Number} link_id
+ */
+ LGraph.prototype.removeLink = function(link_id) {
+ var link = this.links[link_id];
+ if (!link) {
+ return;
+ }
+ var node = this.getNodeById(link.target_id);
+ if (node) {
+ node.disconnectInput(link.target_slot);
+ }
+ };
+
+ //save and recover app state ***************************************
+ /**
+ * Creates a Object containing all the info about this graph, it can be serialized
+ * @method serialize
+ * @return {Object} value of the node
+ */
+ LGraph.prototype.serialize = function() {
+ var nodes_info = [];
+ for (var i = 0, l = this._nodes.length; i < l; ++i) {
+ nodes_info.push(this._nodes[i].serialize());
+ }
+
+ //pack link info into a non-verbose format
+ var links = [];
+ for (var i in this.links) {
+ //links is an OBJECT
+ var link = this.links[i];
+ if (!link.serialize) {
+ //weird bug I havent solved yet
+ console.warn(
+ "weird LLink bug, link info is not a LLink but a regular object"
+ );
+ var link2 = new LLink();
+ for (var j in link) {
+ link2[j] = link[j];
+ }
+ this.links[i] = link2;
+ link = link2;
+ }
+
+ links.push(link.serialize());
+ }
+
+ var groups_info = [];
+ for (var i = 0; i < this._groups.length; ++i) {
+ groups_info.push(this._groups[i].serialize());
+ }
+
+ var data = {
+ last_node_id: this.last_node_id,
+ last_link_id: this.last_link_id,
+ nodes: nodes_info,
+ links: links,
+ groups: groups_info,
+ config: this.config,
+ extra: this.extra,
+ version: LiteGraph.VERSION
+ };
+
+ if(this.onSerialize)
+ this.onSerialize(data);
+
+ return data;
+ };
+
+ /**
+ * Configure a graph from a JSON string
+ * @method configure
+ * @param {String} str configure a graph from a JSON string
+ * @param {Boolean} returns if there was any error parsing
+ */
+ LGraph.prototype.configure = function(data, keep_old) {
+ if (!data) {
+ return;
+ }
+
+ if (!keep_old) {
+ this.clear();
+ }
+
+ var nodes = data.nodes;
+
+ //decode links info (they are very verbose)
+ if (data.links && data.links.constructor === Array) {
+ var links = [];
+ for (var i = 0; i < data.links.length; ++i) {
+ var link_data = data.links[i];
+ if(!link_data) //weird bug
+ {
+ console.warn("serialized graph link data contains errors, skipping.");
+ continue;
+ }
+ var link = new LLink();
+ link.configure(link_data);
+ links[link.id] = link;
+ }
+ data.links = links;
+ }
+
+ //copy all stored fields
+ for (var i in data) {
+ if(i == "nodes" || i == "groups" ) //links must be accepted
+ continue;
+ this[i] = data[i];
+ }
+
+ var error = false;
+
+ //create nodes
+ this._nodes = [];
+ if (nodes) {
+ for (var i = 0, l = nodes.length; i < l; ++i) {
+ var n_info = nodes[i]; //stored info
+ var node = LiteGraph.createNode(n_info.type, n_info.title);
+ if (!node) {
+ if (LiteGraph.debug) {
+ console.log(
+ "Node not found or has errors: " + n_info.type
+ );
+ }
+
+ //in case of error we create a replacement node to avoid losing info
+ node = new LGraphNode();
+ node.last_serialization = n_info;
+ node.has_errors = true;
+ error = true;
+ //continue;
+ }
+
+ node.id = n_info.id; //id it or it will create a new id
+ this.add(node, true); //add before configure, otherwise configure cannot create links
+ }
+
+ //configure nodes afterwards so they can reach each other
+ for (var i = 0, l = nodes.length; i < l; ++i) {
+ var n_info = nodes[i];
+ var node = this.getNodeById(n_info.id);
+ if (node) {
+ node.configure(n_info);
+ }
+ }
+ }
+
+ //groups
+ this._groups.length = 0;
+ if (data.groups) {
+ for (var i = 0; i < data.groups.length; ++i) {
+ var group = new LiteGraph.LGraphGroup();
+ group.configure(data.groups[i]);
+ this.add(group);
+ }
+ }
+
+ this.updateExecutionOrder();
+
+ this.extra = data.extra || {};
+
+ if(this.onConfigure)
+ this.onConfigure(data);
+
+ this._version++;
+ this.setDirtyCanvas(true, true);
+ return error;
+ };
+
+ LGraph.prototype.load = function(url, callback) {
+ var that = this;
+
+ //from file
+ if(url.constructor === File || url.constructor === Blob)
+ {
+ var reader = new FileReader();
+ reader.addEventListener('load', function(event) {
+ var data = JSON.parse(event.target.result);
+ that.configure(data);
+ if(callback)
+ callback();
+ });
+
+ reader.readAsText(url);
+ return;
+ }
+
+ //is a string, then an URL
+ var req = new XMLHttpRequest();
+ req.open("GET", url, true);
+ req.send(null);
+ req.onload = function(oEvent) {
+ if (req.status !== 200) {
+ console.error("Error loading graph:", req.status, req.response);
+ return;
+ }
+ var data = JSON.parse( req.response );
+ that.configure(data);
+ if(callback)
+ callback();
+ };
+ req.onerror = function(err) {
+ console.error("Error loading graph:", err);
+ };
+ };
+
+ LGraph.prototype.onNodeTrace = function(node, msg, color) {
+ //TODO
+ };
+
+ //this is the class in charge of storing link information
+ function LLink(id, type, origin_id, origin_slot, target_id, target_slot) {
+ this.id = id;
+ this.type = type;
+ this.origin_id = origin_id;
+ this.origin_slot = origin_slot;
+ this.target_id = target_id;
+ this.target_slot = target_slot;
+
+ this._data = null;
+ this._pos = new Float32Array(2); //center
+ }
+
+ LLink.prototype.configure = function(o) {
+ if (o.constructor === Array) {
+ this.id = o[0];
+ this.origin_id = o[1];
+ this.origin_slot = o[2];
+ this.target_id = o[3];
+ this.target_slot = o[4];
+ this.type = o[5];
+ } else {
+ this.id = o.id;
+ this.type = o.type;
+ this.origin_id = o.origin_id;
+ this.origin_slot = o.origin_slot;
+ this.target_id = o.target_id;
+ this.target_slot = o.target_slot;
+ }
+ };
+
+ LLink.prototype.serialize = function() {
+ return [
+ this.id,
+ this.origin_id,
+ this.origin_slot,
+ this.target_id,
+ this.target_slot,
+ this.type
+ ];
+ };
+
+ LiteGraph.LLink = LLink;
+
+ // *************************************************************
+ // Node CLASS *******
+ // *************************************************************
+
+ /*
+ title: string
+ pos: [x,y]
+ size: [x,y]
+
+ input|output: every connection
+ + { name:string, type:string, pos: [x,y]=Optional, direction: "input"|"output", links: Array });
+
+ general properties:
+ + clip_area: if you render outside the node, it will be clipped
+ + unsafe_execution: not allowed for safe execution
+ + skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected
+ + resizable: if set to false it wont be resizable with the mouse
+ + horizontal: slots are distributed horizontally
+ + widgets_start_y: widgets start at y distance from the top of the node
+
+ flags object:
+ + collapsed: if it is collapsed
+
+ supported callbacks:
+ + onAdded: when added to graph (warning: this is called BEFORE the node is configured when loading)
+ + onRemoved: when removed from graph
+ + onStart: when the graph starts playing
+ + onStop: when the graph stops playing
+ + onDrawForeground: render the inside widgets inside the node
+ + onDrawBackground: render the background area inside the node (only in edit mode)
+ + onMouseDown
+ + onMouseMove
+ + onMouseUp
+ + onMouseEnter
+ + onMouseLeave
+ + onExecute: execute the node
+ + onPropertyChanged: when a property is changed in the panel (return true to skip default behaviour)
+ + onGetInputs: returns an array of possible inputs
+ + onGetOutputs: returns an array of possible outputs
+ + onBounding: in case this node has a bigger bounding than the node itself (the callback receives the bounding as [x,y,w,h])
+ + onDblClick: double clicked in the node
+ + onInputDblClick: input slot double clicked (can be used to automatically create a node connected)
+ + onOutputDblClick: output slot double clicked (can be used to automatically create a node connected)
+ + onConfigure: called after the node has been configured
+ + onSerialize: to add extra info when serializing (the callback receives the object that should be filled with the data)
+ + onSelected
+ + onDeselected
+ + onDropItem : DOM item dropped over the node
+ + onDropFile : file dropped over the node
+ + onConnectInput : if returns false the incoming connection will be canceled
+ + onConnectionsChange : a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info )
+ + onAction: action slot triggered
+ + getExtraMenuOptions: to add option to context menu
+*/
+
+ /**
+ * Base Class for all the node type classes
+ * @class LGraphNode
+ * @param {String} name a name for the node
+ */
+
+ function LGraphNode(title) {
+ this._ctor(title);
+ }
+
+ global.LGraphNode = LiteGraph.LGraphNode = LGraphNode;
+
+ LGraphNode.prototype._ctor = function(title) {
+ this.title = title || "Unnamed";
+ this.size = [LiteGraph.NODE_WIDTH, 60];
+ this.graph = null;
+
+ this._pos = new Float32Array(10, 10);
+
+ Object.defineProperty(this, "pos", {
+ set: function(v) {
+ if (!v || v.length < 2) {
+ return;
+ }
+ this._pos[0] = v[0];
+ this._pos[1] = v[1];
+ },
+ get: function() {
+ return this._pos;
+ },
+ enumerable: true
+ });
+
+ if (LiteGraph.use_uuids) {
+ this.id = LiteGraph.uuidv4();
+ }
+ else {
+ this.id = -1; //not know till not added
+ }
+ this.type = null;
+
+ //inputs available: array of inputs
+ this.inputs = [];
+ this.outputs = [];
+ this.connections = [];
+
+ //local data
+ this.properties = {}; //for the values
+ this.properties_info = []; //for the info
+
+ this.flags = {};
+ };
+
+ /**
+ * configure a node from an object containing the serialized info
+ * @method configure
+ */
+ LGraphNode.prototype.configure = function(info) {
+ if (this.graph) {
+ this.graph._version++;
+ }
+ for (var j in info) {
+ if (j == "properties") {
+ //i don't want to clone properties, I want to reuse the old container
+ for (var k in info.properties) {
+ this.properties[k] = info.properties[k];
+ if (this.onPropertyChanged) {
+ this.onPropertyChanged( k, info.properties[k] );
+ }
+ }
+ continue;
+ }
+
+ if (info[j] == null) {
+ continue;
+ } else if (typeof info[j] == "object") {
+ //object
+ if (this[j] && this[j].configure) {
+ this[j].configure(info[j]);
+ } else {
+ this[j] = LiteGraph.cloneObject(info[j], this[j]);
+ }
+ } //value
+ else {
+ this[j] = info[j];
+ }
+ }
+
+ if (!info.title) {
+ this.title = this.constructor.title;
+ }
+
+ if (this.inputs) {
+ for (var i = 0; i < this.inputs.length; ++i) {
+ var input = this.inputs[i];
+ var link_info = this.graph ? this.graph.links[input.link] : null;
+ if (this.onConnectionsChange)
+ this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated
+
+ if( this.onInputAdded )
+ this.onInputAdded(input);
+
+ }
+ }
+
+ if (this.outputs) {
+ for (var i = 0; i < this.outputs.length; ++i) {
+ var output = this.outputs[i];
+ if (!output.links) {
+ continue;
+ }
+ for (var j = 0; j < output.links.length; ++j) {
+ var link_info = this.graph ? this.graph.links[output.links[j]] : null;
+ if (this.onConnectionsChange)
+ this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated
+ }
+
+ if( this.onOutputAdded )
+ this.onOutputAdded(output);
+ }
+ }
+
+ if( this.widgets )
+ {
+ for (var i = 0; i < this.widgets.length; ++i)
+ {
+ var w = this.widgets[i];
+ if(!w)
+ continue;
+ if(w.options && w.options.property && (this.properties[ w.options.property ] != undefined))
+ w.value = JSON.parse( JSON.stringify( this.properties[ w.options.property ] ) );
+ }
+ if (info.widgets_values) {
+ for (var i = 0; i < info.widgets_values.length; ++i) {
+ if (this.widgets[i]) {
+ this.widgets[i].value = info.widgets_values[i];
+ }
+ }
+ }
+ }
+
+ if (this.onConfigure) {
+ this.onConfigure(info);
+ }
+ };
+
+ /**
+ * serialize the content
+ * @method serialize
+ */
+
+ LGraphNode.prototype.serialize = function() {
+ //create serialization object
+ var o = {
+ id: this.id,
+ type: this.type,
+ pos: this.pos,
+ size: this.size,
+ flags: LiteGraph.cloneObject(this.flags),
+ order: this.order,
+ mode: this.mode
+ };
+
+ //special case for when there were errors
+ if (this.constructor === LGraphNode && this.last_serialization) {
+ return this.last_serialization;
+ }
+
+ if (this.inputs) {
+ o.inputs = this.inputs;
+ }
+
+ if (this.outputs) {
+ //clear outputs last data (because data in connections is never serialized but stored inside the outputs info)
+ for (var i = 0; i < this.outputs.length; i++) {
+ delete this.outputs[i]._data;
+ }
+ o.outputs = this.outputs;
+ }
+
+ if (this.title && this.title != this.constructor.title) {
+ o.title = this.title;
+ }
+
+ if (this.properties) {
+ o.properties = LiteGraph.cloneObject(this.properties);
+ }
+
+ if (this.widgets && this.serialize_widgets) {
+ o.widgets_values = [];
+ for (var i = 0; i < this.widgets.length; ++i) {
+ if(this.widgets[i])
+ o.widgets_values[i] = this.widgets[i].value;
+ else
+ o.widgets_values[i] = null;
+ }
+ }
+
+ if (!o.type) {
+ o.type = this.constructor.type;
+ }
+
+ if (this.color) {
+ o.color = this.color;
+ }
+ if (this.bgcolor) {
+ o.bgcolor = this.bgcolor;
+ }
+ if (this.boxcolor) {
+ o.boxcolor = this.boxcolor;
+ }
+ if (this.shape) {
+ o.shape = this.shape;
+ }
+
+ if (this.onSerialize) {
+ if (this.onSerialize(o)) {
+ console.warn(
+ "node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter"
+ );
+ }
+ }
+
+ return o;
+ };
+
+ /* Creates a clone of this node */
+ LGraphNode.prototype.clone = function() {
+ var node = LiteGraph.createNode(this.type);
+ if (!node) {
+ return null;
+ }
+
+ //we clone it because serialize returns shared containers
+ var data = LiteGraph.cloneObject(this.serialize());
+
+ //remove links
+ if (data.inputs) {
+ for (var i = 0; i < data.inputs.length; ++i) {
+ data.inputs[i].link = null;
+ }
+ }
+
+ if (data.outputs) {
+ for (var i = 0; i < data.outputs.length; ++i) {
+ if (data.outputs[i].links) {
+ data.outputs[i].links.length = 0;
+ }
+ }
+ }
+
+ delete data["id"];
+
+ if (LiteGraph.use_uuids) {
+ data["id"] = LiteGraph.uuidv4()
+ }
+
+ //remove links
+ node.configure(data);
+
+ return node;
+ };
+
+ /**
+ * serialize and stringify
+ * @method toString
+ */
+
+ LGraphNode.prototype.toString = function() {
+ return JSON.stringify(this.serialize());
+ };
+ //LGraphNode.prototype.deserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph
+
+ /**
+ * get the title string
+ * @method getTitle
+ */
+
+ LGraphNode.prototype.getTitle = function() {
+ return this.title || this.constructor.title;
+ };
+
+ /**
+ * sets the value of a property
+ * @method setProperty
+ * @param {String} name
+ * @param {*} value
+ */
+ LGraphNode.prototype.setProperty = function(name, value) {
+ if (!this.properties) {
+ this.properties = {};
+ }
+ if( value === this.properties[name] )
+ return;
+ var prev_value = this.properties[name];
+ this.properties[name] = value;
+ if (this.onPropertyChanged) {
+ if( this.onPropertyChanged(name, value, prev_value) === false ) //abort change
+ this.properties[name] = prev_value;
+ }
+ if(this.widgets) //widgets could be linked to properties
+ for(var i = 0; i < this.widgets.length; ++i)
+ {
+ var w = this.widgets[i];
+ if(!w)
+ continue;
+ if(w.options.property == name)
+ {
+ w.value = value;
+ break;
+ }
+ }
+ };
+
+ // Execution *************************
+ /**
+ * sets the output data
+ * @method setOutputData
+ * @param {number} slot
+ * @param {*} data
+ */
+ LGraphNode.prototype.setOutputData = function(slot, data) {
+ if (!this.outputs) {
+ return;
+ }
+
+ //this maybe slow and a niche case
+ //if(slot && slot.constructor === String)
+ // slot = this.findOutputSlot(slot);
+
+ if (slot == -1 || slot >= this.outputs.length) {
+ return;
+ }
+
+ var output_info = this.outputs[slot];
+ if (!output_info) {
+ return;
+ }
+
+ //store data in the output itself in case we want to debug
+ output_info._data = data;
+
+ //if there are connections, pass the data to the connections
+ if (this.outputs[slot].links) {
+ for (var i = 0; i < this.outputs[slot].links.length; i++) {
+ var link_id = this.outputs[slot].links[i];
+ var link = this.graph.links[link_id];
+ if(link)
+ link.data = data;
+ }
+ }
+ };
+
+ /**
+ * sets the output data type, useful when you want to be able to overwrite the data type
+ * @method setOutputDataType
+ * @param {number} slot
+ * @param {String} datatype
+ */
+ LGraphNode.prototype.setOutputDataType = function(slot, type) {
+ if (!this.outputs) {
+ return;
+ }
+ if (slot == -1 || slot >= this.outputs.length) {
+ return;
+ }
+ var output_info = this.outputs[slot];
+ if (!output_info) {
+ return;
+ }
+ //store data in the output itself in case we want to debug
+ output_info.type = type;
+
+ //if there are connections, pass the data to the connections
+ if (this.outputs[slot].links) {
+ for (var i = 0; i < this.outputs[slot].links.length; i++) {
+ var link_id = this.outputs[slot].links[i];
+ this.graph.links[link_id].type = type;
+ }
+ }
+ };
+
+ /**
+ * Retrieves the input data (data traveling through the connection) from one slot
+ * @method getInputData
+ * @param {number} slot
+ * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link
+ * @return {*} data or if it is not connected returns undefined
+ */
+ LGraphNode.prototype.getInputData = function(slot, force_update) {
+ if (!this.inputs) {
+ return;
+ } //undefined;
+
+ if (slot >= this.inputs.length || this.inputs[slot].link == null) {
+ return;
+ }
+
+ var link_id = this.inputs[slot].link;
+ var link = this.graph.links[link_id];
+ if (!link) {
+ //bug: weird case but it happens sometimes
+ return null;
+ }
+
+ if (!force_update) {
+ return link.data;
+ }
+
+ //special case: used to extract data from the incoming connection before the graph has been executed
+ var node = this.graph.getNodeById(link.origin_id);
+ if (!node) {
+ return link.data;
+ }
+
+ if (node.updateOutputData) {
+ node.updateOutputData(link.origin_slot);
+ } else if (node.onExecute) {
+ node.onExecute();
+ }
+
+ return link.data;
+ };
+
+ /**
+ * Retrieves the input data type (in case this supports multiple input types)
+ * @method getInputDataType
+ * @param {number} slot
+ * @return {String} datatype in string format
+ */
+ LGraphNode.prototype.getInputDataType = function(slot) {
+ if (!this.inputs) {
+ return null;
+ } //undefined;
+
+ if (slot >= this.inputs.length || this.inputs[slot].link == null) {
+ return null;
+ }
+ var link_id = this.inputs[slot].link;
+ var link = this.graph.links[link_id];
+ if (!link) {
+ //bug: weird case but it happens sometimes
+ return null;
+ }
+ var node = this.graph.getNodeById(link.origin_id);
+ if (!node) {
+ return link.type;
+ }
+ var output_info = node.outputs[link.origin_slot];
+ if (output_info) {
+ return output_info.type;
+ }
+ return null;
+ };
+
+ /**
+ * Retrieves the input data from one slot using its name instead of slot number
+ * @method getInputDataByName
+ * @param {String} slot_name
+ * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link
+ * @return {*} data or if it is not connected returns null
+ */
+ LGraphNode.prototype.getInputDataByName = function(
+ slot_name,
+ force_update
+ ) {
+ var slot = this.findInputSlot(slot_name);
+ if (slot == -1) {
+ return null;
+ }
+ return this.getInputData(slot, force_update);
+ };
+
+ /**
+ * tells you if there is a connection in one input slot
+ * @method isInputConnected
+ * @param {number} slot
+ * @return {boolean}
+ */
+ LGraphNode.prototype.isInputConnected = function(slot) {
+ if (!this.inputs) {
+ return false;
+ }
+ return slot < this.inputs.length && this.inputs[slot].link != null;
+ };
+
+ /**
+ * tells you info about an input connection (which node, type, etc)
+ * @method getInputInfo
+ * @param {number} slot
+ * @return {Object} object or null { link: id, name: string, type: string or 0 }
+ */
+ LGraphNode.prototype.getInputInfo = function(slot) {
+ if (!this.inputs) {
+ return null;
+ }
+ if (slot < this.inputs.length) {
+ return this.inputs[slot];
+ }
+ return null;
+ };
+
+ /**
+ * Returns the link info in the connection of an input slot
+ * @method getInputLink
+ * @param {number} slot
+ * @return {LLink} object or null
+ */
+ LGraphNode.prototype.getInputLink = function(slot) {
+ if (!this.inputs) {
+ return null;
+ }
+ if (slot < this.inputs.length) {
+ var slot_info = this.inputs[slot];
+ return this.graph.links[ slot_info.link ];
+ }
+ return null;
+ };
+
+ /**
+ * returns the node connected in the input slot
+ * @method getInputNode
+ * @param {number} slot
+ * @return {LGraphNode} node or null
+ */
+ LGraphNode.prototype.getInputNode = function(slot) {
+ if (!this.inputs) {
+ return null;
+ }
+ if (slot >= this.inputs.length) {
+ return null;
+ }
+ var input = this.inputs[slot];
+ if (!input || input.link === null) {
+ return null;
+ }
+ var link_info = this.graph.links[input.link];
+ if (!link_info) {
+ return null;
+ }
+ return this.graph.getNodeById(link_info.origin_id);
+ };
+
+ /**
+ * returns the value of an input with this name, otherwise checks if there is a property with that name
+ * @method getInputOrProperty
+ * @param {string} name
+ * @return {*} value
+ */
+ LGraphNode.prototype.getInputOrProperty = function(name) {
+ if (!this.inputs || !this.inputs.length) {
+ return this.properties ? this.properties[name] : null;
+ }
+
+ for (var i = 0, l = this.inputs.length; i < l; ++i) {
+ var input_info = this.inputs[i];
+ if (name == input_info.name && input_info.link != null) {
+ var link = this.graph.links[input_info.link];
+ if (link) {
+ return link.data;
+ }
+ }
+ }
+ return this.properties[name];
+ };
+
+ /**
+ * tells you the last output data that went in that slot
+ * @method getOutputData
+ * @param {number} slot
+ * @return {Object} object or null
+ */
+ LGraphNode.prototype.getOutputData = function(slot) {
+ if (!this.outputs) {
+ return null;
+ }
+ if (slot >= this.outputs.length) {
+ return null;
+ }
+
+ var info = this.outputs[slot];
+ return info._data;
+ };
+
+ /**
+ * tells you info about an output connection (which node, type, etc)
+ * @method getOutputInfo
+ * @param {number} slot
+ * @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] }
+ */
+ LGraphNode.prototype.getOutputInfo = function(slot) {
+ if (!this.outputs) {
+ return null;
+ }
+ if (slot < this.outputs.length) {
+ return this.outputs[slot];
+ }
+ return null;
+ };
+
+ /**
+ * tells you if there is a connection in one output slot
+ * @method isOutputConnected
+ * @param {number} slot
+ * @return {boolean}
+ */
+ LGraphNode.prototype.isOutputConnected = function(slot) {
+ if (!this.outputs) {
+ return false;
+ }
+ return (
+ slot < this.outputs.length &&
+ this.outputs[slot].links &&
+ this.outputs[slot].links.length
+ );
+ };
+
+ /**
+ * tells you if there is any connection in the output slots
+ * @method isAnyOutputConnected
+ * @return {boolean}
+ */
+ LGraphNode.prototype.isAnyOutputConnected = function() {
+ if (!this.outputs) {
+ return false;
+ }
+ for (var i = 0; i < this.outputs.length; ++i) {
+ if (this.outputs[i].links && this.outputs[i].links.length) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ /**
+ * retrieves all the nodes connected to this output slot
+ * @method getOutputNodes
+ * @param {number} slot
+ * @return {array}
+ */
+ LGraphNode.prototype.getOutputNodes = function(slot) {
+ if (!this.outputs || this.outputs.length == 0) {
+ return null;
+ }
+
+ if (slot >= this.outputs.length) {
+ return null;
+ }
+
+ var output = this.outputs[slot];
+ if (!output.links || output.links.length == 0) {
+ return null;
+ }
+
+ var r = [];
+ for (var i = 0; i < output.links.length; i++) {
+ var link_id = output.links[i];
+ var link = this.graph.links[link_id];
+ if (link) {
+ var target_node = this.graph.getNodeById(link.target_id);
+ if (target_node) {
+ r.push(target_node);
+ }
+ }
+ }
+ return r;
+ };
+
+ LGraphNode.prototype.addOnTriggerInput = function(){
+ var trigS = this.findInputSlot("onTrigger");
+ if (trigS == -1){ //!trigS ||
+ var input = this.addInput("onTrigger", LiteGraph.EVENT, {optional: true, nameLocked: true});
+ return this.findInputSlot("onTrigger");
+ }
+ return trigS;
+ }
+
+ LGraphNode.prototype.addOnExecutedOutput = function(){
+ var trigS = this.findOutputSlot("onExecuted");
+ if (trigS == -1){ //!trigS ||
+ var output = this.addOutput("onExecuted", LiteGraph.ACTION, {optional: true, nameLocked: true});
+ return this.findOutputSlot("onExecuted");
+ }
+ return trigS;
+ }
+
+ LGraphNode.prototype.onAfterExecuteNode = function(param, options){
+ var trigS = this.findOutputSlot("onExecuted");
+ if (trigS != -1){
+
+ //console.debug(this.id+":"+this.order+" triggering slot onAfterExecute");
+ //console.debug(param);
+ //console.debug(options);
+ this.triggerSlot(trigS, param, null, options);
+
+ }
+ }
+
+ LGraphNode.prototype.changeMode = function(modeTo){
+ switch(modeTo){
+ case LiteGraph.ON_EVENT:
+ // this.addOnExecutedOutput();
+ break;
+
+ case LiteGraph.ON_TRIGGER:
+ this.addOnTriggerInput();
+ this.addOnExecutedOutput();
+ break;
+
+ case LiteGraph.NEVER:
+ break;
+
+ case LiteGraph.ALWAYS:
+ break;
+
+ case LiteGraph.ON_REQUEST:
+ break;
+
+ default:
+ return false;
+ break;
+ }
+ this.mode = modeTo;
+ return true;
+ };
+
+ /**
+ * Triggers the node code execution, place a boolean/counter to mark the node as being executed
+ * @method execute
+ * @param {*} param
+ * @param {*} options
+ */
+ LGraphNode.prototype.doExecute = function(param, options) {
+ options = options || {};
+ if (this.onExecute){
+
+ // enable this to give the event an ID
+ if (!options.action_call) options.action_call = this.id+"_exec_"+Math.floor(Math.random()*9999);
+
+ this.graph.nodes_executing[this.id] = true; //.push(this.id);
+
+ this.onExecute(param, options);
+
+ this.graph.nodes_executing[this.id] = false; //.pop();
+
+ // save execution/action ref
+ this.exec_version = this.graph.iteration;
+ if(options && options.action_call){
+ this.action_call = options.action_call; // if (param)
+ this.graph.nodes_executedAction[this.id] = options.action_call;
+ }
+ }
+ this.execute_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event
+ if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); // callback
+ };
+
+ /**
+ * Triggers an action, wrapped by logics to control execution flow
+ * @method actionDo
+ * @param {String} action name
+ * @param {*} param
+ */
+ LGraphNode.prototype.actionDo = function(action, param, options) {
+ options = options || {};
+ if (this.onAction){
+
+ // enable this to give the event an ID
+ if (!options.action_call) options.action_call = this.id+"_"+(action?action:"action")+"_"+Math.floor(Math.random()*9999);
+
+ this.graph.nodes_actioning[this.id] = (action?action:"actioning"); //.push(this.id);
+
+ this.onAction(action, param, options);
+
+ this.graph.nodes_actioning[this.id] = false; //.pop();
+
+ // save execution/action ref
+ if(options && options.action_call){
+ this.action_call = options.action_call; // if (param)
+ this.graph.nodes_executedAction[this.id] = options.action_call;
+ }
+ }
+ this.action_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event
+ if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options);
+ };
+
+ /**
+ * Triggers an event in this node, this will trigger any output with the same name
+ * @method trigger
+ * @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all
+ * @param {*} param
+ */
+ LGraphNode.prototype.trigger = function(action, param, options) {
+ if (!this.outputs || !this.outputs.length) {
+ return;
+ }
+
+ if (this.graph)
+ this.graph._last_trigger_time = LiteGraph.getTime();
+
+ for (var i = 0; i < this.outputs.length; ++i) {
+ var output = this.outputs[i];
+ if ( !output || output.type !== LiteGraph.EVENT || (action && output.name != action) )
+ continue;
+ this.triggerSlot(i, param, null, options);
+ }
+ };
+
+ /**
+ * Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes
+ * @method triggerSlot
+ * @param {Number} slot the index of the output slot
+ * @param {*} param
+ * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot
+ */
+ LGraphNode.prototype.triggerSlot = function(slot, param, link_id, options) {
+ options = options || {};
+ if (!this.outputs) {
+ return;
+ }
+
+ if(slot == null)
+ {
+ console.error("slot must be a number");
+ return;
+ }
+
+ if(slot.constructor !== Number)
+ console.warn("slot must be a number, use node.trigger('name') if you want to use a string");
+
+ var output = this.outputs[slot];
+ if (!output) {
+ return;
+ }
+
+ var links = output.links;
+ if (!links || !links.length) {
+ return;
+ }
+
+ if (this.graph) {
+ this.graph._last_trigger_time = LiteGraph.getTime();
+ }
+
+ //for every link attached here
+ for (var k = 0; k < links.length; ++k) {
+ var id = links[k];
+ if (link_id != null && link_id != id) {
+ //to skip links
+ continue;
+ }
+ var link_info = this.graph.links[links[k]];
+ if (!link_info) {
+ //not connected
+ continue;
+ }
+ link_info._last_time = LiteGraph.getTime();
+ var node = this.graph.getNodeById(link_info.target_id);
+ if (!node) {
+ //node not found?
+ continue;
+ }
+
+ //used to mark events in graph
+ var target_connection = node.inputs[link_info.target_slot];
+
+ if (node.mode === LiteGraph.ON_TRIGGER)
+ {
+ // generate unique trigger ID if not present
+ if (!options.action_call) options.action_call = this.id+"_trigg_"+Math.floor(Math.random()*9999);
+ if (node.onExecute) {
+ // -- wrapping node.onExecute(param); --
+ node.doExecute(param, options);
+ }
+ }
+ else if (node.onAction) {
+ // generate unique action ID if not present
+ if (!options.action_call) options.action_call = this.id+"_act_"+Math.floor(Math.random()*9999);
+ //pass the action name
+ var target_connection = node.inputs[link_info.target_slot];
+ // wrap node.onAction(target_connection.name, param);
+ node.actionDo(target_connection.name, param, options);
+ }
+ }
+ };
+
+ /**
+ * clears the trigger slot animation
+ * @method clearTriggeredSlot
+ * @param {Number} slot the index of the output slot
+ * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot
+ */
+ LGraphNode.prototype.clearTriggeredSlot = function(slot, link_id) {
+ if (!this.outputs) {
+ return;
+ }
+
+ var output = this.outputs[slot];
+ if (!output) {
+ return;
+ }
+
+ var links = output.links;
+ if (!links || !links.length) {
+ return;
+ }
+
+ //for every link attached here
+ for (var k = 0; k < links.length; ++k) {
+ var id = links[k];
+ if (link_id != null && link_id != id) {
+ //to skip links
+ continue;
+ }
+ var link_info = this.graph.links[links[k]];
+ if (!link_info) {
+ //not connected
+ continue;
+ }
+ link_info._last_time = 0;
+ }
+ };
+
+ /**
+ * changes node size and triggers callback
+ * @method setSize
+ * @param {vec2} size
+ */
+ LGraphNode.prototype.setSize = function(size)
+ {
+ this.size = size;
+ if(this.onResize)
+ this.onResize(this.size);
+ }
+
+ /**
+ * add a new property to this node
+ * @method addProperty
+ * @param {string} name
+ * @param {*} default_value
+ * @param {string} type string defining the output type ("vec3","number",...)
+ * @param {Object} extra_info this can be used to have special properties of the property (like values, etc)
+ */
+ LGraphNode.prototype.addProperty = function(
+ name,
+ default_value,
+ type,
+ extra_info
+ ) {
+ var o = { name: name, type: type, default_value: default_value };
+ if (extra_info) {
+ for (var i in extra_info) {
+ o[i] = extra_info[i];
+ }
+ }
+ if (!this.properties_info) {
+ this.properties_info = [];
+ }
+ this.properties_info.push(o);
+ if (!this.properties) {
+ this.properties = {};
+ }
+ this.properties[name] = default_value;
+ return o;
+ };
+
+ //connections
+
+ /**
+ * add a new output slot to use in this node
+ * @method addOutput
+ * @param {string} name
+ * @param {string} type string defining the output type ("vec3","number",...)
+ * @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc)
+ */
+ LGraphNode.prototype.addOutput = function(name, type, extra_info) {
+ var output = { name: name, type: type, links: null };
+ if (extra_info) {
+ for (var i in extra_info) {
+ output[i] = extra_info[i];
+ }
+ }
+
+ if (!this.outputs) {
+ this.outputs = [];
+ }
+ this.outputs.push(output);
+ if (this.onOutputAdded) {
+ this.onOutputAdded(output);
+ }
+
+ if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,type,true);
+
+ this.setSize( this.computeSize() );
+ this.setDirtyCanvas(true, true);
+ return output;
+ };
+
+ /**
+ * add a new output slot to use in this node
+ * @method addOutputs
+ * @param {Array} array of triplets like [[name,type,extra_info],[...]]
+ */
+ LGraphNode.prototype.addOutputs = function(array) {
+ for (var i = 0; i < array.length; ++i) {
+ var info = array[i];
+ var o = { name: info[0], type: info[1], link: null };
+ if (array[2]) {
+ for (var j in info[2]) {
+ o[j] = info[2][j];
+ }
+ }
+
+ if (!this.outputs) {
+ this.outputs = [];
+ }
+ this.outputs.push(o);
+ if (this.onOutputAdded) {
+ this.onOutputAdded(o);
+ }
+
+ if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,info[1],true);
+
+ }
+
+ this.setSize( this.computeSize() );
+ this.setDirtyCanvas(true, true);
+ };
+
+ /**
+ * remove an existing output slot
+ * @method removeOutput
+ * @param {number} slot
+ */
+ LGraphNode.prototype.removeOutput = function(slot) {
+ this.disconnectOutput(slot);
+ this.outputs.splice(slot, 1);
+ for (var i = slot; i < this.outputs.length; ++i) {
+ if (!this.outputs[i] || !this.outputs[i].links) {
+ continue;
+ }
+ var links = this.outputs[i].links;
+ for (var j = 0; j < links.length; ++j) {
+ var link = this.graph.links[links[j]];
+ if (!link) {
+ continue;
+ }
+ link.origin_slot -= 1;
+ }
+ }
+
+ this.setSize( this.computeSize() );
+ if (this.onOutputRemoved) {
+ this.onOutputRemoved(slot);
+ }
+ this.setDirtyCanvas(true, true);
+ };
+
+ /**
+ * add a new input slot to use in this node
+ * @method addInput
+ * @param {string} name
+ * @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0
+ * @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc)
+ */
+ LGraphNode.prototype.addInput = function(name, type, extra_info) {
+ type = type || 0;
+ var input = { name: name, type: type, link: null };
+ if (extra_info) {
+ for (var i in extra_info) {
+ input[i] = extra_info[i];
+ }
+ }
+
+ if (!this.inputs) {
+ this.inputs = [];
+ }
+
+ this.inputs.push(input);
+ this.setSize( this.computeSize() );
+
+ if (this.onInputAdded) {
+ this.onInputAdded(input);
+ }
+
+ LiteGraph.registerNodeAndSlotType(this,type);
+
+ this.setDirtyCanvas(true, true);
+ return input;
+ };
+
+ /**
+ * add several new input slots in this node
+ * @method addInputs
+ * @param {Array} array of triplets like [[name,type,extra_info],[...]]
+ */
+ LGraphNode.prototype.addInputs = function(array) {
+ for (var i = 0; i < array.length; ++i) {
+ var info = array[i];
+ var o = { name: info[0], type: info[1], link: null };
+ if (array[2]) {
+ for (var j in info[2]) {
+ o[j] = info[2][j];
+ }
+ }
+
+ if (!this.inputs) {
+ this.inputs = [];
+ }
+ this.inputs.push(o);
+ if (this.onInputAdded) {
+ this.onInputAdded(o);
+ }
+
+ LiteGraph.registerNodeAndSlotType(this,info[1]);
+ }
+
+ this.setSize( this.computeSize() );
+ this.setDirtyCanvas(true, true);
+ };
+
+ /**
+ * remove an existing input slot
+ * @method removeInput
+ * @param {number} slot
+ */
+ LGraphNode.prototype.removeInput = function(slot) {
+ this.disconnectInput(slot);
+ var slot_info = this.inputs.splice(slot, 1);
+ for (var i = slot; i < this.inputs.length; ++i) {
+ if (!this.inputs[i]) {
+ continue;
+ }
+ var link = this.graph.links[this.inputs[i].link];
+ if (!link) {
+ continue;
+ }
+ link.target_slot -= 1;
+ }
+ this.setSize( this.computeSize() );
+ if (this.onInputRemoved) {
+ this.onInputRemoved(slot, slot_info[0] );
+ }
+ this.setDirtyCanvas(true, true);
+ };
+
+ /**
+ * add an special connection to this node (used for special kinds of graphs)
+ * @method addConnection
+ * @param {string} name
+ * @param {string} type string defining the input type ("vec3","number",...)
+ * @param {[x,y]} pos position of the connection inside the node
+ * @param {string} direction if is input or output
+ */
+ LGraphNode.prototype.addConnection = function(name, type, pos, direction) {
+ var o = {
+ name: name,
+ type: type,
+ pos: pos,
+ direction: direction,
+ links: null
+ };
+ this.connections.push(o);
+ return o;
+ };
+
+ /**
+ * computes the minimum size of a node according to its inputs and output slots
+ * @method computeSize
+ * @param {vec2} minHeight
+ * @return {vec2} the total size
+ */
+ LGraphNode.prototype.computeSize = function(out) {
+ if (this.constructor.size) {
+ return this.constructor.size.concat();
+ }
+
+ var rows = Math.max(
+ this.inputs ? this.inputs.length : 1,
+ this.outputs ? this.outputs.length : 1
+ );
+ var size = out || new Float32Array([0, 0]);
+ rows = Math.max(rows, 1);
+ var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size
+
+ var title_width = compute_text_size(this.title);
+ var input_width = 0;
+ var output_width = 0;
+
+ if (this.inputs) {
+ for (var i = 0, l = this.inputs.length; i < l; ++i) {
+ var input = this.inputs[i];
+ var text = input.label || input.name || "";
+ var text_width = compute_text_size(text);
+ if (input_width < text_width) {
+ input_width = text_width;
+ }
+ }
+ }
+
+ if (this.outputs) {
+ for (var i = 0, l = this.outputs.length; i < l; ++i) {
+ var output = this.outputs[i];
+ var text = output.label || output.name || "";
+ var text_width = compute_text_size(text);
+ if (output_width < text_width) {
+ output_width = text_width;
+ }
+ }
+ }
+
+ size[0] = Math.max(input_width + output_width + 10, title_width);
+ size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH);
+ if (this.widgets && this.widgets.length) {
+ size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5);
+ }
+
+ size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT;
+
+ var widgets_height = 0;
+ if (this.widgets && this.widgets.length) {
+ for (var i = 0, l = this.widgets.length; i < l; ++i) {
+ if (this.widgets[i].computeSize)
+ widgets_height += this.widgets[i].computeSize(size[0])[1] + 4;
+ else
+ widgets_height += LiteGraph.NODE_WIDGET_HEIGHT + 4;
+ }
+ widgets_height += 8;
+ }
+
+ //compute height using widgets height
+ if( this.widgets_up )
+ size[1] = Math.max( size[1], widgets_height );
+ else if( this.widgets_start_y != null )
+ size[1] = Math.max( size[1], widgets_height + this.widgets_start_y );
+ else
+ size[1] += widgets_height;
+
+ function compute_text_size(text) {
+ if (!text) {
+ return 0;
+ }
+ return font_size * text.length * 0.6;
+ }
+
+ if (
+ this.constructor.min_height &&
+ size[1] < this.constructor.min_height
+ ) {
+ size[1] = this.constructor.min_height;
+ }
+
+ size[1] += 6; //margin
+
+ return size;
+ };
+
+ LGraphNode.prototype.inResizeCorner = function(canvasX, canvasY) {
+ var rows = this.outputs ? this.outputs.length : 1;
+ var outputs_offset = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT;
+ return isInsideRectangle(canvasX,
+ canvasY,
+ this.pos[0] + this.size[0] - 15,
+ this.pos[1] + Math.max(this.size[1] - 15, outputs_offset),
+ 20,
+ 20
+ );
+ }
+
+ /**
+ * returns all the info available about a property of this node.
+ *
+ * @method getPropertyInfo
+ * @param {String} property name of the property
+ * @return {Object} the object with all the available info
+ */
+ LGraphNode.prototype.getPropertyInfo = function( property )
+ {
+ var info = null;
+
+ //there are several ways to define info about a property
+ //legacy mode
+ if (this.properties_info) {
+ for (var i = 0; i < this.properties_info.length; ++i) {
+ if (this.properties_info[i].name == property) {
+ info = this.properties_info[i];
+ break;
+ }
+ }
+ }
+ //litescene mode using the constructor
+ if(this.constructor["@" + property])
+ info = this.constructor["@" + property];
+
+ if(this.constructor.widgets_info && this.constructor.widgets_info[property])
+ info = this.constructor.widgets_info[property];
+
+ //litescene mode using the constructor
+ if (!info && this.onGetPropertyInfo) {
+ info = this.onGetPropertyInfo(property);
+ }
+
+ if (!info)
+ info = {};
+ if(!info.type)
+ info.type = typeof this.properties[property];
+ if(info.widget == "combo")
+ info.type = "enum";
+
+ return info;
+ }
+
+ /**
+ * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties
+ *
+ * @method addWidget
+ * @param {String} type the widget type (could be "number","string","combo"
+ * @param {String} name the text to show on the widget
+ * @param {String} value the default value
+ * @param {Function|String} callback function to call when it changes (optionally, it can be the name of the property to modify)
+ * @param {Object} options the object that contains special properties of this widget
+ * @return {Object} the created widget object
+ */
+ LGraphNode.prototype.addWidget = function( type, name, value, callback, options )
+ {
+ if (!this.widgets) {
+ this.widgets = [];
+ }
+
+ if(!options && callback && callback.constructor === Object)
+ {
+ options = callback;
+ callback = null;
+ }
+
+ if(options && options.constructor === String) //options can be the property name
+ options = { property: options };
+
+ if(callback && callback.constructor === String) //callback can be the property name
+ {
+ if(!options)
+ options = {};
+ options.property = callback;
+ callback = null;
+ }
+
+ if(callback && callback.constructor !== Function)
+ {
+ console.warn("addWidget: callback must be a function");
+ callback = null;
+ }
+
+ var w = {
+ type: type.toLowerCase(),
+ name: name,
+ value: value,
+ callback: callback,
+ options: options || {}
+ };
+
+ if (w.options.y !== undefined) {
+ w.y = w.options.y;
+ }
+
+ if (!callback && !w.options.callback && !w.options.property) {
+ console.warn("LiteGraph addWidget(...) without a callback or property assigned");
+ }
+ if (type == "combo" && !w.options.values) {
+ throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }";
+ }
+ this.widgets.push(w);
+ this.setSize( this.computeSize() );
+ return w;
+ };
+
+ LGraphNode.prototype.addCustomWidget = function(custom_widget) {
+ if (!this.widgets) {
+ this.widgets = [];
+ }
+ this.widgets.push(custom_widget);
+ return custom_widget;
+ };
+
+ /**
+ * returns the bounding of the object, used for rendering purposes
+ * @method getBounding
+ * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage
+ * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation
+ * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height]
+ */
+ LGraphNode.prototype.getBounding = function(out, compute_outer) {
+ out = out || new Float32Array(4);
+ const nodePos = this.pos;
+ const isCollapsed = this.flags.collapsed;
+ const nodeSize = this.size;
+
+ let left_offset = 0;
+ // 1 offset due to how nodes are rendered
+ let right_offset = 1 ;
+ let top_offset = 0;
+ let bottom_offset = 0;
+
+ if (compute_outer) {
+ // 4 offset for collapsed node connection points
+ left_offset = 4;
+ // 6 offset for right shadow and collapsed node connection points
+ right_offset = 6 + left_offset;
+ // 4 offset for collapsed nodes top connection points
+ top_offset = 4;
+ // 5 offset for bottom shadow and collapsed node connection points
+ bottom_offset = 5 + top_offset;
+ }
+
+ out[0] = nodePos[0] - left_offset;
+ out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset;
+ out[2] = isCollapsed ?
+ (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset :
+ nodeSize[0] + right_offset;
+ out[3] = isCollapsed ?
+ LiteGraph.NODE_TITLE_HEIGHT + bottom_offset :
+ nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset;
+
+ if (this.onBounding) {
+ this.onBounding(out);
+ }
+ return out;
+ };
+
+ /**
+ * checks if a point is inside the shape of a node
+ * @method isPointInside
+ * @param {number} x
+ * @param {number} y
+ * @return {boolean}
+ */
+ LGraphNode.prototype.isPointInside = function(x, y, margin, skip_title) {
+ margin = margin || 0;
+
+ var margin_top = this.graph && this.graph.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT;
+ if (skip_title) {
+ margin_top = 0;
+ }
+ if (this.flags && this.flags.collapsed) {
+ //if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS)
+ if (
+ isInsideRectangle(
+ x,
+ y,
+ this.pos[0] - margin,
+ this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin,
+ (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) +
+ 2 * margin,
+ LiteGraph.NODE_TITLE_HEIGHT + 2 * margin
+ )
+ ) {
+ return true;
+ }
+ } else if (
+ this.pos[0] - 4 - margin < x &&
+ this.pos[0] + this.size[0] + 4 + margin > x &&
+ this.pos[1] - margin_top - margin < y &&
+ this.pos[1] + this.size[1] + margin > y
+ ) {
+ return true;
+ }
+ return false;
+ };
+
+ /**
+ * checks if a point is inside a node slot, and returns info about which slot
+ * @method getSlotInPosition
+ * @param {number} x
+ * @param {number} y
+ * @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] }
+ */
+ LGraphNode.prototype.getSlotInPosition = function(x, y) {
+ //search for inputs
+ var link_pos = new Float32Array(2);
+ if (this.inputs) {
+ for (var i = 0, l = this.inputs.length; i < l; ++i) {
+ var input = this.inputs[i];
+ this.getConnectionPos(true, i, link_pos);
+ if (
+ isInsideRectangle(
+ x,
+ y,
+ link_pos[0] - 10,
+ link_pos[1] - 5,
+ 20,
+ 10
+ )
+ ) {
+ return { input: input, slot: i, link_pos: link_pos };
+ }
+ }
+ }
+
+ if (this.outputs) {
+ for (var i = 0, l = this.outputs.length; i < l; ++i) {
+ var output = this.outputs[i];
+ this.getConnectionPos(false, i, link_pos);
+ if (
+ isInsideRectangle(
+ x,
+ y,
+ link_pos[0] - 10,
+ link_pos[1] - 5,
+ 20,
+ 10
+ )
+ ) {
+ return { output: output, slot: i, link_pos: link_pos };
+ }
+ }
+ }
+
+ return null;
+ };
+
+ /**
+ * returns the input slot with a given name (used for dynamic slots), -1 if not found
+ * @method findInputSlot
+ * @param {string} name the name of the slot
+ * @param {boolean} returnObj if the obj itself wanted
+ * @return {number_or_object} the slot (-1 if not found)
+ */
+ LGraphNode.prototype.findInputSlot = function(name, returnObj) {
+ if (!this.inputs) {
+ return -1;
+ }
+ for (var i = 0, l = this.inputs.length; i < l; ++i) {
+ if (name == this.inputs[i].name) {
+ return !returnObj ? i : this.inputs[i];
+ }
+ }
+ return -1;
+ };
+
+ /**
+ * returns the output slot with a given name (used for dynamic slots), -1 if not found
+ * @method findOutputSlot
+ * @param {string} name the name of the slot
+ * @param {boolean} returnObj if the obj itself wanted
+ * @return {number_or_object} the slot (-1 if not found)
+ */
+ LGraphNode.prototype.findOutputSlot = function(name, returnObj) {
+ returnObj = returnObj || false;
+ if (!this.outputs) {
+ return -1;
+ }
+ for (var i = 0, l = this.outputs.length; i < l; ++i) {
+ if (name == this.outputs[i].name) {
+ return !returnObj ? i : this.outputs[i];
+ }
+ }
+ return -1;
+ };
+
+ // TODO refactor: USE SINGLE findInput/findOutput functions! :: merge options
+
+ /**
+ * returns the first free input slot
+ * @method findInputSlotFree
+ * @param {object} options
+ * @return {number_or_object} the slot (-1 if not found)
+ */
+ LGraphNode.prototype.findInputSlotFree = function(optsIn) {
+ var optsIn = optsIn || {};
+ var optsDef = {returnObj: false
+ ,typesNotAccepted: []
+ };
+ var opts = Object.assign(optsDef,optsIn);
+ if (!this.inputs) {
+ return -1;
+ }
+ for (var i = 0, l = this.inputs.length; i < l; ++i) {
+ if (this.inputs[i].link && this.inputs[i].link != null) {
+ continue;
+ }
+ if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.inputs[i].type)){
+ continue;
+ }
+ return !opts.returnObj ? i : this.inputs[i];
+ }
+ return -1;
+ };
+
+ /**
+ * returns the first output slot free
+ * @method findOutputSlotFree
+ * @param {object} options
+ * @return {number_or_object} the slot (-1 if not found)
+ */
+ LGraphNode.prototype.findOutputSlotFree = function(optsIn) {
+ var optsIn = optsIn || {};
+ var optsDef = { returnObj: false
+ ,typesNotAccepted: []
+ };
+ var opts = Object.assign(optsDef,optsIn);
+ if (!this.outputs) {
+ return -1;
+ }
+ for (var i = 0, l = this.outputs.length; i < l; ++i) {
+ if (this.outputs[i].links && this.outputs[i].links != null) {
+ continue;
+ }
+ if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.outputs[i].type)){
+ continue;
+ }
+ return !opts.returnObj ? i : this.outputs[i];
+ }
+ return -1;
+ };
+
+ /**
+ * findSlotByType for INPUTS
+ */
+ LGraphNode.prototype.findInputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) {
+ return this.findSlotByType(true, type, returnObj, preferFreeSlot, doNotUseOccupied);
+ };
+
+ /**
+ * findSlotByType for OUTPUTS
+ */
+ LGraphNode.prototype.findOutputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) {
+ return this.findSlotByType(false, type, returnObj, preferFreeSlot, doNotUseOccupied);
+ };
+
+ /**
+ * returns the output (or input) slot with a given type, -1 if not found
+ * @method findSlotByType
+ * @param {boolean} input uise inputs instead of outputs
+ * @param {string} type the type of the slot
+ * @param {boolean} returnObj if the obj itself wanted
+ * @param {boolean} preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway)
+ * @return {number_or_object} the slot (-1 if not found)
+ */
+ LGraphNode.prototype.findSlotByType = function(input, type, returnObj, preferFreeSlot, doNotUseOccupied) {
+ input = input || false;
+ returnObj = returnObj || false;
+ preferFreeSlot = preferFreeSlot || false;
+ doNotUseOccupied = doNotUseOccupied || false;
+ var aSlots = input ? this.inputs : this.outputs;
+ if (!aSlots) {
+ return -1;
+ }
+ // !! empty string type is considered 0, * !!
+ if (type == "" || type == "*") type = 0;
+ for (var i = 0, l = aSlots.length; i < l; ++i) {
+ var tFound = false;
+ var aSource = (type+"").toLowerCase().split(",");
+ var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type;
+ aDest = (aDest+"").toLowerCase().split(",");
+ for(var sI=0;sI= 0 && target_slot !== null){
+ //console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot)
+ return this.connect(slot, target_node, target_slot);
+ }else{
+ //console.log("type "+target_slotType+" not found or not free?")
+ if (opts.createEventInCase && target_slotType == LiteGraph.EVENT){
+ // WILL CREATE THE onTrigger IN SLOT
+ //console.debug("connect WILL CREATE THE onTrigger "+target_slotType+" to "+target_node);
+ return this.connect(slot, target_node, -1);
+ }
+ // connect to the first general output slot if not found a specific type and
+ if (opts.generalTypeInCase){
+ var target_slot = target_node.findInputSlotByType(0, false, true, true);
+ //console.debug("connect TO a general type (*, 0), if not found the specific type ",target_slotType," to ",target_node,"RES_SLOT:",target_slot);
+ if (target_slot >= 0){
+ return this.connect(slot, target_node, target_slot);
+ }
+ }
+ // connect to the first free input slot if not found a specific type and this output is general
+ if (opts.firstFreeIfOutputGeneralInCase && (target_slotType == 0 || target_slotType == "*" || target_slotType == "")){
+ var target_slot = target_node.findInputSlotFree({typesNotAccepted: [LiteGraph.EVENT] });
+ //console.debug("connect TO TheFirstFREE ",target_slotType," to ",target_node,"RES_SLOT:",target_slot);
+ if (target_slot >= 0){
+ return this.connect(slot, target_node, target_slot);
+ }
+ }
+
+ console.debug("no way to connect type: ",target_slotType," to targetNODE ",target_node);
+ //TODO filter
+
+ return null;
+ }
+ }
+
+ /**
+ * connect this node input to the output of another node BY TYPE
+ * @method connectByType
+ * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
+ * @param {LGraphNode} node the target node
+ * @param {string} target_type the output slot type of the target node
+ * @return {Object} the link_info is created, otherwise null
+ */
+ LGraphNode.prototype.connectByTypeOutput = function(slot, source_node, source_slotType, optsIn) {
+ var optsIn = optsIn || {};
+ var optsDef = { createEventInCase: true
+ ,firstFreeIfInputGeneralInCase: true
+ ,generalTypeInCase: true
+ };
+ var opts = Object.assign(optsDef,optsIn);
+ if (source_node && source_node.constructor === Number) {
+ source_node = this.graph.getNodeById(source_node);
+ }
+ var source_slot = source_node.findOutputSlotByType(source_slotType, false, true);
+ if (source_slot >= 0 && source_slot !== null){
+ //console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot)
+ return source_node.connect(source_slot, this, slot);
+ }else{
+
+ // connect to the first general output slot if not found a specific type and
+ if (opts.generalTypeInCase){
+ var source_slot = source_node.findOutputSlotByType(0, false, true, true);
+ if (source_slot >= 0){
+ return source_node.connect(source_slot, this, slot);
+ }
+ }
+
+ if (opts.createEventInCase && source_slotType == LiteGraph.EVENT){
+ // WILL CREATE THE onExecuted OUT SLOT
+ if (LiteGraph.do_add_triggers_slots){
+ var source_slot = source_node.addOnExecutedOutput();
+ return source_node.connect(source_slot, this, slot);
+ }
+ }
+ // connect to the first free output slot if not found a specific type and this input is general
+ if (opts.firstFreeIfInputGeneralInCase && (source_slotType == 0 || source_slotType == "*" || source_slotType == "")){
+ var source_slot = source_node.findOutputSlotFree({typesNotAccepted: [LiteGraph.EVENT] });
+ if (source_slot >= 0){
+ return source_node.connect(source_slot, this, slot);
+ }
+ }
+
+ console.debug("no way to connect byOUT type: ",source_slotType," to sourceNODE ",source_node);
+ //TODO filter
+
+ //console.log("type OUT! "+source_slotType+" not found or not free?")
+ return null;
+ }
+ }
+
+ /**
+ * connect this node output to the input of another node
+ * @method connect
+ * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
+ * @param {LGraphNode} node the target node
+ * @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger)
+ * @return {Object} the link_info is created, otherwise null
+ */
+ LGraphNode.prototype.connect = function(slot, target_node, target_slot) {
+ target_slot = target_slot || 0;
+
+ if (!this.graph) {
+ //could be connected before adding it to a graph
+ console.log(
+ "Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them."
+ ); //due to link ids being associated with graphs
+ return null;
+ }
+
+ //seek for the output slot
+ if (slot.constructor === String) {
+ slot = this.findOutputSlot(slot);
+ if (slot == -1) {
+ if (LiteGraph.debug) {
+ console.log("Connect: Error, no slot of name " + slot);
+ }
+ return null;
+ }
+ } else if (!this.outputs || slot >= this.outputs.length) {
+ if (LiteGraph.debug) {
+ console.log("Connect: Error, slot number not found");
+ }
+ return null;
+ }
+
+ if (target_node && target_node.constructor === Number) {
+ target_node = this.graph.getNodeById(target_node);
+ }
+ if (!target_node) {
+ throw "target node is null";
+ }
+
+ //avoid loopback
+ if (target_node == this) {
+ return null;
+ }
+
+ //you can specify the slot by name
+ if (target_slot.constructor === String) {
+ target_slot = target_node.findInputSlot(target_slot);
+ if (target_slot == -1) {
+ if (LiteGraph.debug) {
+ console.log(
+ "Connect: Error, no slot of name " + target_slot
+ );
+ }
+ return null;
+ }
+ } else if (target_slot === LiteGraph.EVENT) {
+
+ if (LiteGraph.do_add_triggers_slots){
+ //search for first slot with event? :: NO this is done outside
+ //console.log("Connect: Creating triggerEvent");
+ // force mode
+ target_node.changeMode(LiteGraph.ON_TRIGGER);
+ target_slot = target_node.findInputSlot("onTrigger");
+ }else{
+ return null; // -- break --
+ }
+ } else if (
+ !target_node.inputs ||
+ target_slot >= target_node.inputs.length
+ ) {
+ if (LiteGraph.debug) {
+ console.log("Connect: Error, slot number not found");
+ }
+ return null;
+ }
+
+ var changed = false;
+
+ var input = target_node.inputs[target_slot];
+ var link_info = null;
+ var output = this.outputs[slot];
+
+ if (!this.outputs[slot]){
+ /*console.debug("Invalid slot passed: "+slot);
+ console.debug(this.outputs);*/
+ return null;
+ }
+
+ // allow target node to change slot
+ if (target_node.onBeforeConnectInput) {
+ // This way node can choose another slot (or make a new one?)
+ target_slot = target_node.onBeforeConnectInput(target_slot); //callback
+ }
+
+ //check target_slot and check connection types
+ if (target_slot===false || target_slot===null || !LiteGraph.isValidConnection(output.type, input.type))
+ {
+ this.setDirtyCanvas(false, true);
+ if(changed)
+ this.graph.connectionChange(this, link_info);
+ return null;
+ }else{
+ //console.debug("valid connection",output.type, input.type);
+ }
+
+ //allows nodes to block connection, callback
+ if (target_node.onConnectInput) {
+ if ( target_node.onConnectInput(target_slot, output.type, output, this, slot) === false ) {
+ return null;
+ }
+ }
+ if (this.onConnectOutput) { // callback
+ if ( this.onConnectOutput(slot, input.type, input, target_node, target_slot) === false ) {
+ return null;
+ }
+ }
+
+ //if there is something already plugged there, disconnect
+ if (target_node.inputs[target_slot] && target_node.inputs[target_slot].link != null) {
+ this.graph.beforeChange();
+ target_node.disconnectInput(target_slot, {doProcessChange: false});
+ changed = true;
+ }
+ if (output.links !== null && output.links.length){
+ switch(output.type){
+ case LiteGraph.EVENT:
+ if (!LiteGraph.allow_multi_output_for_events){
+ this.graph.beforeChange();
+ this.disconnectOutput(slot, false, {doProcessChange: false}); // Input(target_slot, {doProcessChange: false});
+ changed = true;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ var nextId
+ if (LiteGraph.use_uuids)
+ nextId = LiteGraph.uuidv4();
+ else
+ nextId = ++this.graph.last_link_id;
+
+ //create link class
+ link_info = new LLink(
+ nextId,
+ input.type || output.type,
+ this.id,
+ slot,
+ target_node.id,
+ target_slot
+ );
+
+ //add to graph links list
+ this.graph.links[link_info.id] = link_info;
+
+ //connect in output
+ if (output.links == null) {
+ output.links = [];
+ }
+ output.links.push(link_info.id);
+ //connect in input
+ target_node.inputs[target_slot].link = link_info.id;
+ if (this.graph) {
+ this.graph._version++;
+ }
+ if (this.onConnectionsChange) {
+ this.onConnectionsChange(
+ LiteGraph.OUTPUT,
+ slot,
+ true,
+ link_info,
+ output
+ );
+ } //link_info has been created now, so its updated
+ if (target_node.onConnectionsChange) {
+ target_node.onConnectionsChange(
+ LiteGraph.INPUT,
+ target_slot,
+ true,
+ link_info,
+ input
+ );
+ }
+ if (this.graph && this.graph.onNodeConnectionChange) {
+ this.graph.onNodeConnectionChange(
+ LiteGraph.INPUT,
+ target_node,
+ target_slot,
+ this,
+ slot
+ );
+ this.graph.onNodeConnectionChange(
+ LiteGraph.OUTPUT,
+ this,
+ slot,
+ target_node,
+ target_slot
+ );
+ }
+
+ this.setDirtyCanvas(false, true);
+ this.graph.afterChange();
+ this.graph.connectionChange(this, link_info);
+
+ return link_info;
+ };
+
+ /**
+ * disconnect one output to an specific node
+ * @method disconnectOutput
+ * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
+ * @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected]
+ * @return {boolean} if it was disconnected successfully
+ */
+ LGraphNode.prototype.disconnectOutput = function(slot, target_node) {
+ if (slot.constructor === String) {
+ slot = this.findOutputSlot(slot);
+ if (slot == -1) {
+ if (LiteGraph.debug) {
+ console.log("Connect: Error, no slot of name " + slot);
+ }
+ return false;
+ }
+ } else if (!this.outputs || slot >= this.outputs.length) {
+ if (LiteGraph.debug) {
+ console.log("Connect: Error, slot number not found");
+ }
+ return false;
+ }
+
+ //get output slot
+ var output = this.outputs[slot];
+ if (!output || !output.links || output.links.length == 0) {
+ return false;
+ }
+
+ //one of the output links in this slot
+ if (target_node) {
+ if (target_node.constructor === Number) {
+ target_node = this.graph.getNodeById(target_node);
+ }
+ if (!target_node) {
+ throw "Target Node not found";
+ }
+
+ for (var i = 0, l = output.links.length; i < l; i++) {
+ var link_id = output.links[i];
+ var link_info = this.graph.links[link_id];
+
+ //is the link we are searching for...
+ if (link_info.target_id == target_node.id) {
+ output.links.splice(i, 1); //remove here
+ var input = target_node.inputs[link_info.target_slot];
+ input.link = null; //remove there
+ delete this.graph.links[link_id]; //remove the link from the links pool
+ if (this.graph) {
+ this.graph._version++;
+ }
+ if (target_node.onConnectionsChange) {
+ target_node.onConnectionsChange(
+ LiteGraph.INPUT,
+ link_info.target_slot,
+ false,
+ link_info,
+ input
+ );
+ } //link_info hasn't been modified so its ok
+ if (this.onConnectionsChange) {
+ this.onConnectionsChange(
+ LiteGraph.OUTPUT,
+ slot,
+ false,
+ link_info,
+ output
+ );
+ }
+ if (this.graph && this.graph.onNodeConnectionChange) {
+ this.graph.onNodeConnectionChange(
+ LiteGraph.OUTPUT,
+ this,
+ slot
+ );
+ }
+ if (this.graph && this.graph.onNodeConnectionChange) {
+ this.graph.onNodeConnectionChange(
+ LiteGraph.OUTPUT,
+ this,
+ slot
+ );
+ this.graph.onNodeConnectionChange(
+ LiteGraph.INPUT,
+ target_node,
+ link_info.target_slot
+ );
+ }
+ break;
+ }
+ }
+ } //all the links in this output slot
+ else {
+ for (var i = 0, l = output.links.length; i < l; i++) {
+ var link_id = output.links[i];
+ var link_info = this.graph.links[link_id];
+ if (!link_info) {
+ //bug: it happens sometimes
+ continue;
+ }
+
+ var target_node = this.graph.getNodeById(link_info.target_id);
+ var input = null;
+ if (this.graph) {
+ this.graph._version++;
+ }
+ if (target_node) {
+ input = target_node.inputs[link_info.target_slot];
+ input.link = null; //remove other side link
+ if (target_node.onConnectionsChange) {
+ target_node.onConnectionsChange(
+ LiteGraph.INPUT,
+ link_info.target_slot,
+ false,
+ link_info,
+ input
+ );
+ } //link_info hasn't been modified so its ok
+ if (this.graph && this.graph.onNodeConnectionChange) {
+ this.graph.onNodeConnectionChange(
+ LiteGraph.INPUT,
+ target_node,
+ link_info.target_slot
+ );
+ }
+ }
+ delete this.graph.links[link_id]; //remove the link from the links pool
+ if (this.onConnectionsChange) {
+ this.onConnectionsChange(
+ LiteGraph.OUTPUT,
+ slot,
+ false,
+ link_info,
+ output
+ );
+ }
+ if (this.graph && this.graph.onNodeConnectionChange) {
+ this.graph.onNodeConnectionChange(
+ LiteGraph.OUTPUT,
+ this,
+ slot
+ );
+ this.graph.onNodeConnectionChange(
+ LiteGraph.INPUT,
+ target_node,
+ link_info.target_slot
+ );
+ }
+ }
+ output.links = null;
+ }
+
+ this.setDirtyCanvas(false, true);
+ this.graph.connectionChange(this);
+ return true;
+ };
+
+ /**
+ * disconnect one input
+ * @method disconnectInput
+ * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
+ * @return {boolean} if it was disconnected successfully
+ */
+ LGraphNode.prototype.disconnectInput = function(slot) {
+ //seek for the output slot
+ if (slot.constructor === String) {
+ slot = this.findInputSlot(slot);
+ if (slot == -1) {
+ if (LiteGraph.debug) {
+ console.log("Connect: Error, no slot of name " + slot);
+ }
+ return false;
+ }
+ } else if (!this.inputs || slot >= this.inputs.length) {
+ if (LiteGraph.debug) {
+ console.log("Connect: Error, slot number not found");
+ }
+ return false;
+ }
+
+ var input = this.inputs[slot];
+ if (!input) {
+ return false;
+ }
+
+ var link_id = this.inputs[slot].link;
+ if(link_id != null)
+ {
+ this.inputs[slot].link = null;
+
+ //remove other side
+ var link_info = this.graph.links[link_id];
+ if (link_info) {
+ var target_node = this.graph.getNodeById(link_info.origin_id);
+ if (!target_node) {
+ return false;
+ }
+
+ var output = target_node.outputs[link_info.origin_slot];
+ if (!output || !output.links || output.links.length == 0) {
+ return false;
+ }
+
+ //search in the inputs list for this link
+ for (var i = 0, l = output.links.length; i < l; i++) {
+ if (output.links[i] == link_id) {
+ output.links.splice(i, 1);
+ break;
+ }
+ }
+
+ delete this.graph.links[link_id]; //remove from the pool
+ if (this.graph) {
+ this.graph._version++;
+ }
+ if (this.onConnectionsChange) {
+ this.onConnectionsChange(
+ LiteGraph.INPUT,
+ slot,
+ false,
+ link_info,
+ input
+ );
+ }
+ if (target_node.onConnectionsChange) {
+ target_node.onConnectionsChange(
+ LiteGraph.OUTPUT,
+ i,
+ false,
+ link_info,
+ output
+ );
+ }
+ if (this.graph && this.graph.onNodeConnectionChange) {
+ this.graph.onNodeConnectionChange(
+ LiteGraph.OUTPUT,
+ target_node,
+ i
+ );
+ this.graph.onNodeConnectionChange(LiteGraph.INPUT, this, slot);
+ }
+ }
+ } //link != null
+
+ this.setDirtyCanvas(false, true);
+ if(this.graph)
+ this.graph.connectionChange(this);
+ return true;
+ };
+
+ /**
+ * returns the center of a connection point in canvas coords
+ * @method getConnectionPos
+ * @param {boolean} is_input true if if a input slot, false if it is an output
+ * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
+ * @param {vec2} out [optional] a place to store the output, to free garbage
+ * @return {[x,y]} the position
+ **/
+ LGraphNode.prototype.getConnectionPos = function(
+ is_input,
+ slot_number,
+ out
+ ) {
+ out = out || new Float32Array(2);
+ var num_slots = 0;
+ if (is_input && this.inputs) {
+ num_slots = this.inputs.length;
+ }
+ if (!is_input && this.outputs) {
+ num_slots = this.outputs.length;
+ }
+
+ var offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5;
+
+ if (this.flags.collapsed) {
+ var w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
+ if (this.horizontal) {
+ out[0] = this.pos[0] + w * 0.5;
+ if (is_input) {
+ out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT;
+ } else {
+ out[1] = this.pos[1];
+ }
+ } else {
+ if (is_input) {
+ out[0] = this.pos[0];
+ } else {
+ out[0] = this.pos[0] + w;
+ }
+ out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
+ }
+ return out;
+ }
+
+ //weird feature that never got finished
+ if (is_input && slot_number == -1) {
+ out[0] = this.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * 0.5;
+ out[1] = this.pos[1] + LiteGraph.NODE_TITLE_HEIGHT * 0.5;
+ return out;
+ }
+
+ //hard-coded pos
+ if (
+ is_input &&
+ num_slots > slot_number &&
+ this.inputs[slot_number].pos
+ ) {
+ out[0] = this.pos[0] + this.inputs[slot_number].pos[0];
+ out[1] = this.pos[1] + this.inputs[slot_number].pos[1];
+ return out;
+ } else if (
+ !is_input &&
+ num_slots > slot_number &&
+ this.outputs[slot_number].pos
+ ) {
+ out[0] = this.pos[0] + this.outputs[slot_number].pos[0];
+ out[1] = this.pos[1] + this.outputs[slot_number].pos[1];
+ return out;
+ }
+
+ //horizontal distributed slots
+ if (this.horizontal) {
+ out[0] =
+ this.pos[0] + (slot_number + 0.5) * (this.size[0] / num_slots);
+ if (is_input) {
+ out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT;
+ } else {
+ out[1] = this.pos[1] + this.size[1];
+ }
+ return out;
+ }
+
+ //default vertical slots
+ if (is_input) {
+ out[0] = this.pos[0] + offset;
+ } else {
+ out[0] = this.pos[0] + this.size[0] + 1 - offset;
+ }
+ out[1] =
+ this.pos[1] +
+ (slot_number + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
+ (this.constructor.slot_start_y || 0);
+ return out;
+ };
+
+ /* Force align to grid */
+ LGraphNode.prototype.alignToGrid = function() {
+ this.pos[0] =
+ LiteGraph.CANVAS_GRID_SIZE *
+ Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE);
+ this.pos[1] =
+ LiteGraph.CANVAS_GRID_SIZE *
+ Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE);
+ };
+
+ /* Console output */
+ LGraphNode.prototype.trace = function(msg) {
+ if (!this.console) {
+ this.console = [];
+ }
+
+ this.console.push(msg);
+ if (this.console.length > LGraphNode.MAX_CONSOLE) {
+ this.console.shift();
+ }
+
+ if(this.graph.onNodeTrace)
+ this.graph.onNodeTrace(this, msg);
+ };
+
+ /* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */
+ LGraphNode.prototype.setDirtyCanvas = function(
+ dirty_foreground,
+ dirty_background
+ ) {
+ if (!this.graph) {
+ return;
+ }
+ this.graph.sendActionToCanvas("setDirty", [
+ dirty_foreground,
+ dirty_background
+ ]);
+ };
+
+ LGraphNode.prototype.loadImage = function(url) {
+ var img = new Image();
+ img.src = LiteGraph.node_images_path + url;
+ img.ready = false;
+
+ var that = this;
+ img.onload = function() {
+ this.ready = true;
+ that.setDirtyCanvas(true);
+ };
+ return img;
+ };
+
+ //safe LGraphNode action execution (not sure if safe)
+ /*
+LGraphNode.prototype.executeAction = function(action)
+{
+ if(action == "") return false;
+
+ if( action.indexOf(";") != -1 || action.indexOf("}") != -1)
+ {
+ this.trace("Error: Action contains unsafe characters");
+ return false;
+ }
+
+ var tokens = action.split("(");
+ var func_name = tokens[0];
+ if( typeof(this[func_name]) != "function")
+ {
+ this.trace("Error: Action not found on node: " + func_name);
+ return false;
+ }
+
+ var code = action;
+
+ try
+ {
+ var _foo = eval;
+ eval = null;
+ (new Function("with(this) { " + code + "}")).call(this);
+ eval = _foo;
+ }
+ catch (err)
+ {
+ this.trace("Error executing action {" + action + "} :" + err);
+ return false;
+ }
+
+ return true;
+}
+*/
+
+ /* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */
+ LGraphNode.prototype.captureInput = function(v) {
+ if (!this.graph || !this.graph.list_of_graphcanvas) {
+ return;
+ }
+
+ var list = this.graph.list_of_graphcanvas;
+
+ for (var i = 0; i < list.length; ++i) {
+ var c = list[i];
+ //releasing somebody elses capture?!
+ if (!v && c.node_capturing_input != this) {
+ continue;
+ }
+
+ //change
+ c.node_capturing_input = v ? this : null;
+ }
+ };
+
+ /**
+ * Collapse the node to make it smaller on the canvas
+ * @method collapse
+ **/
+ LGraphNode.prototype.collapse = function(force) {
+ this.graph._version++;
+ if (this.constructor.collapsable === false && !force) {
+ return;
+ }
+ if (!this.flags.collapsed) {
+ this.flags.collapsed = true;
+ } else {
+ this.flags.collapsed = false;
+ }
+ this.setDirtyCanvas(true, true);
+ };
+
+ /**
+ * Forces the node to do not move or realign on Z
+ * @method pin
+ **/
+
+ LGraphNode.prototype.pin = function(v) {
+ this.graph._version++;
+ if (v === undefined) {
+ this.flags.pinned = !this.flags.pinned;
+ } else {
+ this.flags.pinned = v;
+ }
+ };
+
+ LGraphNode.prototype.localToScreen = function(x, y, graphcanvas) {
+ return [
+ (x + this.pos[0]) * graphcanvas.scale + graphcanvas.offset[0],
+ (y + this.pos[1]) * graphcanvas.scale + graphcanvas.offset[1]
+ ];
+ };
+
+ function LGraphGroup(title) {
+ this._ctor(title);
+ }
+
+ global.LGraphGroup = LiteGraph.LGraphGroup = LGraphGroup;
+
+ LGraphGroup.prototype._ctor = function(title) {
+ this.title = title || "Group";
+ this.font_size = 24;
+ this.color = LGraphCanvas.node_colors.pale_blue
+ ? LGraphCanvas.node_colors.pale_blue.groupcolor
+ : "#AAA";
+ this._bounding = new Float32Array([10, 10, 140, 80]);
+ this._pos = this._bounding.subarray(0, 2);
+ this._size = this._bounding.subarray(2, 4);
+ this._nodes = [];
+ this.graph = null;
+
+ Object.defineProperty(this, "pos", {
+ set: function(v) {
+ if (!v || v.length < 2) {
+ return;
+ }
+ this._pos[0] = v[0];
+ this._pos[1] = v[1];
+ },
+ get: function() {
+ return this._pos;
+ },
+ enumerable: true
+ });
+
+ Object.defineProperty(this, "size", {
+ set: function(v) {
+ if (!v || v.length < 2) {
+ return;
+ }
+ this._size[0] = Math.max(140, v[0]);
+ this._size[1] = Math.max(80, v[1]);
+ },
+ get: function() {
+ return this._size;
+ },
+ enumerable: true
+ });
+ };
+
+ LGraphGroup.prototype.configure = function(o) {
+ this.title = o.title;
+ this._bounding.set(o.bounding);
+ this.color = o.color;
+ if (o.font_size) {
+ this.font_size = o.font_size;
+ }
+ };
+
+ LGraphGroup.prototype.serialize = function() {
+ var b = this._bounding;
+ return {
+ title: this.title,
+ bounding: [
+ Math.round(b[0]),
+ Math.round(b[1]),
+ Math.round(b[2]),
+ Math.round(b[3])
+ ],
+ color: this.color,
+ font_size: this.font_size
+ };
+ };
+
+ LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) {
+ this._pos[0] += deltax;
+ this._pos[1] += deltay;
+ if (ignore_nodes) {
+ return;
+ }
+ for (var i = 0; i < this._nodes.length; ++i) {
+ var node = this._nodes[i];
+ node.pos[0] += deltax;
+ node.pos[1] += deltay;
+ }
+ };
+
+ LGraphGroup.prototype.recomputeInsideNodes = function() {
+ this._nodes.length = 0;
+ var nodes = this.graph._nodes;
+ var node_bounding = new Float32Array(4);
+
+ for (var i = 0; i < nodes.length; ++i) {
+ var node = nodes[i];
+ node.getBounding(node_bounding);
+ if (!overlapBounding(this._bounding, node_bounding)) {
+ continue;
+ } //out of the visible area
+ this._nodes.push(node);
+ }
+ };
+
+ LGraphGroup.prototype.isPointInside = LGraphNode.prototype.isPointInside;
+ LGraphGroup.prototype.setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas;
+
+ //****************************************
+
+ //Scale and Offset
+ function DragAndScale(element, skip_events) {
+ this.offset = new Float32Array([0, 0]);
+ this.scale = 1;
+ this.max_scale = 10;
+ this.min_scale = 0.1;
+ this.onredraw = null;
+ this.enabled = true;
+ this.last_mouse = [0, 0];
+ this.element = null;
+ this.visible_area = new Float32Array(4);
+
+ if (element) {
+ this.element = element;
+ if (!skip_events) {
+ this.bindEvents(element);
+ }
+ }
+ }
+
+ LiteGraph.DragAndScale = DragAndScale;
+
+ DragAndScale.prototype.bindEvents = function(element) {
+ this.last_mouse = new Float32Array(2);
+
+ this._binded_mouse_callback = this.onMouse.bind(this);
+
+ LiteGraph.pointerListenerAdd(element,"down", this._binded_mouse_callback);
+ LiteGraph.pointerListenerAdd(element,"move", this._binded_mouse_callback);
+ LiteGraph.pointerListenerAdd(element,"up", this._binded_mouse_callback);
+
+ element.addEventListener(
+ "mousewheel",
+ this._binded_mouse_callback,
+ false
+ );
+ element.addEventListener("wheel", this._binded_mouse_callback, false);
+ };
+
+ DragAndScale.prototype.computeVisibleArea = function( viewport ) {
+ if (!this.element) {
+ this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0;
+ return;
+ }
+ var width = this.element.width;
+ var height = this.element.height;
+ var startx = -this.offset[0];
+ var starty = -this.offset[1];
+ if( viewport )
+ {
+ startx += viewport[0] / this.scale;
+ starty += viewport[1] / this.scale;
+ width = viewport[2];
+ height = viewport[3];
+ }
+ var endx = startx + width / this.scale;
+ var endy = starty + height / this.scale;
+ this.visible_area[0] = startx;
+ this.visible_area[1] = starty;
+ this.visible_area[2] = endx - startx;
+ this.visible_area[3] = endy - starty;
+ };
+
+ DragAndScale.prototype.onMouse = function(e) {
+ if (!this.enabled) {
+ return;
+ }
+
+ var canvas = this.element;
+ var rect = canvas.getBoundingClientRect();
+ var x = e.clientX - rect.left;
+ var y = e.clientY - rect.top;
+ e.canvasx = x;
+ e.canvasy = y;
+ e.dragging = this.dragging;
+
+ var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) );
+
+ //console.log("pointerevents: DragAndScale onMouse "+e.type+" "+is_inside);
+
+ var ignore = false;
+ if (this.onmouse) {
+ ignore = this.onmouse(e);
+ }
+
+ if (e.type == LiteGraph.pointerevents_method+"down" && is_inside) {
+ this.dragging = true;
+ LiteGraph.pointerListenerRemove(canvas,"move",this._binded_mouse_callback);
+ LiteGraph.pointerListenerAdd(document,"move",this._binded_mouse_callback);
+ LiteGraph.pointerListenerAdd(document,"up",this._binded_mouse_callback);
+ } else if (e.type == LiteGraph.pointerevents_method+"move") {
+ if (!ignore) {
+ var deltax = x - this.last_mouse[0];
+ var deltay = y - this.last_mouse[1];
+ if (this.dragging) {
+ this.mouseDrag(deltax, deltay);
+ }
+ }
+ } else if (e.type == LiteGraph.pointerevents_method+"up") {
+ this.dragging = false;
+ LiteGraph.pointerListenerRemove(document,"move",this._binded_mouse_callback);
+ LiteGraph.pointerListenerRemove(document,"up",this._binded_mouse_callback);
+ LiteGraph.pointerListenerAdd(canvas,"move",this._binded_mouse_callback);
+ } else if ( is_inside &&
+ (e.type == "mousewheel" ||
+ e.type == "wheel" ||
+ e.type == "DOMMouseScroll")
+ ) {
+ e.eventType = "mousewheel";
+ if (e.type == "wheel") {
+ e.wheel = -e.deltaY;
+ } else {
+ e.wheel =
+ e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60;
+ }
+
+ //from stack overflow
+ e.delta = e.wheelDelta
+ ? e.wheelDelta / 40
+ : e.deltaY
+ ? -e.deltaY / 3
+ : 0;
+ this.changeDeltaScale(1.0 + e.delta * 0.05);
+ }
+
+ this.last_mouse[0] = x;
+ this.last_mouse[1] = y;
+
+ if(is_inside)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+ };
+
+ DragAndScale.prototype.toCanvasContext = function(ctx) {
+ ctx.scale(this.scale, this.scale);
+ ctx.translate(this.offset[0], this.offset[1]);
+ };
+
+ DragAndScale.prototype.convertOffsetToCanvas = function(pos) {
+ //return [pos[0] / this.scale - this.offset[0], pos[1] / this.scale - this.offset[1]];
+ return [
+ (pos[0] + this.offset[0]) * this.scale,
+ (pos[1] + this.offset[1]) * this.scale
+ ];
+ };
+
+ DragAndScale.prototype.convertCanvasToOffset = function(pos, out) {
+ out = out || [0, 0];
+ out[0] = pos[0] / this.scale - this.offset[0];
+ out[1] = pos[1] / this.scale - this.offset[1];
+ return out;
+ };
+
+ DragAndScale.prototype.mouseDrag = function(x, y) {
+ this.offset[0] += x / this.scale;
+ this.offset[1] += y / this.scale;
+
+ if (this.onredraw) {
+ this.onredraw(this);
+ }
+ };
+
+ DragAndScale.prototype.changeScale = function(value, zooming_center) {
+ if (value < this.min_scale) {
+ value = this.min_scale;
+ } else if (value > this.max_scale) {
+ value = this.max_scale;
+ }
+
+ if (value == this.scale) {
+ return;
+ }
+
+ if (!this.element) {
+ return;
+ }
+
+ var rect = this.element.getBoundingClientRect();
+ if (!rect) {
+ return;
+ }
+
+ zooming_center = zooming_center || [
+ rect.width * 0.5,
+ rect.height * 0.5
+ ];
+ var center = this.convertCanvasToOffset(zooming_center);
+ this.scale = value;
+ if (Math.abs(this.scale - 1) < 0.01) {
+ this.scale = 1;
+ }
+
+ var new_center = this.convertCanvasToOffset(zooming_center);
+ var delta_offset = [
+ new_center[0] - center[0],
+ new_center[1] - center[1]
+ ];
+
+ this.offset[0] += delta_offset[0];
+ this.offset[1] += delta_offset[1];
+
+ if (this.onredraw) {
+ this.onredraw(this);
+ }
+ };
+
+ DragAndScale.prototype.changeDeltaScale = function(value, zooming_center) {
+ this.changeScale(this.scale * value, zooming_center);
+ };
+
+ DragAndScale.prototype.reset = function() {
+ this.scale = 1;
+ this.offset[0] = 0;
+ this.offset[1] = 0;
+ };
+
+ //*********************************************************************************
+ // LGraphCanvas: LGraph renderer CLASS
+ //*********************************************************************************
+
+ /**
+ * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required.
+ * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked
+ *
+ * @class LGraphCanvas
+ * @constructor
+ * @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself)
+ * @param {LGraph} graph [optional]
+ * @param {Object} options [optional] { skip_rendering, autoresize, viewport }
+ */
+ function LGraphCanvas(canvas, graph, options) {
+ this.options = options = options || {};
+
+ //if(graph === undefined)
+ // throw ("No graph assigned");
+ this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE;
+
+ if (canvas && canvas.constructor === String) {
+ canvas = document.querySelector(canvas);
+ }
+
+ this.ds = new DragAndScale();
+ this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much
+
+ this.title_text_font = "" + LiteGraph.NODE_TEXT_SIZE + "px Arial";
+ this.inner_text_font =
+ "normal " + LiteGraph.NODE_SUBTEXT_SIZE + "px Arial";
+ this.node_title_color = LiteGraph.NODE_TITLE_COLOR;
+ this.default_link_color = LiteGraph.LINK_COLOR;
+ this.default_connection_color = {
+ input_off: "#778",
+ input_on: "#7F7", //"#BBD"
+ output_off: "#778",
+ output_on: "#7F7" //"#BBD"
+ };
+ this.default_connection_color_byType = {
+ /*number: "#7F7",
+ string: "#77F",
+ boolean: "#F77",*/
+ }
+ this.default_connection_color_byTypeOff = {
+ /*number: "#474",
+ string: "#447",
+ boolean: "#744",*/
+ };
+
+ this.highquality_render = true;
+ this.use_gradients = false; //set to true to render titlebar with gradients
+ this.editor_alpha = 1; //used for transition
+ this.pause_rendering = false;
+ this.clear_background = true;
+ this.clear_background_color = "#222";
+
+ this.read_only = false; //if set to true users cannot modify the graph
+ this.render_only_selected = true;
+ this.live_mode = false;
+ this.show_info = true;
+ this.allow_dragcanvas = true;
+ this.allow_dragnodes = true;
+ this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc
+ this.multi_select = false; //allow selecting multi nodes without pressing extra keys
+ this.allow_searchbox = true;
+ this.allow_reconnect_links = true; //allows to change a connection with having to redo it again
+ this.align_to_grid = false; //snap to grid
+
+ this.drag_mode = false;
+ this.dragging_rectangle = null;
+
+ this.filter = null; //allows to filter to only accept some type of nodes in a graph
+
+ this.set_canvas_dirty_on_mouse_event = true; //forces to redraw the canvas if the mouse does anything
+ this.always_render_background = false;
+ this.render_shadows = true;
+ this.render_canvas_border = true;
+ this.render_connections_shadows = false; //too much cpu
+ this.render_connections_border = true;
+ this.render_curved_connections = false;
+ this.render_connection_arrows = false;
+ this.render_collapsed_slots = true;
+ this.render_execution_order = false;
+ this.render_title_colored = true;
+ this.render_link_tooltip = true;
+
+ this.links_render_mode = LiteGraph.SPLINE_LINK;
+
+ this.mouse = [0, 0]; //mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle
+ this.graph_mouse = [0, 0]; //mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle
+ this.canvas_mouse = this.graph_mouse; //LEGACY: REMOVE THIS, USE GRAPH_MOUSE INSTEAD
+
+ //to personalize the search box
+ this.onSearchBox = null;
+ this.onSearchBoxSelection = null;
+
+ //callbacks
+ this.onMouse = null;
+ this.onDrawBackground = null; //to render background objects (behind nodes and connections) in the canvas affected by transform
+ this.onDrawForeground = null; //to render foreground objects (above nodes and connections) in the canvas affected by transform
+ this.onDrawOverlay = null; //to render foreground objects not affected by transform (for GUIs)
+ this.onDrawLinkTooltip = null; //called when rendering a tooltip
+ this.onNodeMoved = null; //called after moving a node
+ this.onSelectionChange = null; //called if the selection changes
+ this.onConnectingChange = null; //called before any link changes
+ this.onBeforeChange = null; //called before modifying the graph
+ this.onAfterChange = null; //called after modifying the graph
+
+ this.connections_width = 3;
+ this.round_radius = 8;
+
+ this.current_node = null;
+ this.node_widget = null; //used for widgets
+ this.over_link_center = null;
+ this.last_mouse_position = [0, 0];
+ this.visible_area = this.ds.visible_area;
+ this.visible_links = [];
+
+ this.viewport = options.viewport || null; //to constraint render area to a portion of the canvas
+
+ //link canvas and graph
+ if (graph) {
+ graph.attachCanvas(this);
+ }
+
+ this.setCanvas(canvas,options.skip_events);
+ this.clear();
+
+ if (!options.skip_render) {
+ this.startRendering();
+ }
+
+ this.autoresize = options.autoresize;
+ }
+
+ global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas;
+
+ LGraphCanvas.DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=";
+
+ LGraphCanvas.link_type_colors = {
+ "-1": LiteGraph.EVENT_LINK_COLOR,
+ number: "#AAA",
+ node: "#DCA"
+ };
+ LGraphCanvas.gradients = {}; //cache of gradients
+
+ /**
+ * clears all the data inside
+ *
+ * @method clear
+ */
+ LGraphCanvas.prototype.clear = function() {
+ this.frame = 0;
+ this.last_draw_time = 0;
+ this.render_time = 0;
+ this.fps = 0;
+
+ //this.scale = 1;
+ //this.offset = [0,0];
+
+ this.dragging_rectangle = null;
+
+ this.selected_nodes = {};
+ this.selected_group = null;
+
+ this.visible_nodes = [];
+ this.node_dragged = null;
+ this.node_over = null;
+ this.node_capturing_input = null;
+ this.connecting_node = null;
+ this.highlighted_links = {};
+
+ this.dragging_canvas = false;
+
+ this.dirty_canvas = true;
+ this.dirty_bgcanvas = true;
+ this.dirty_area = null;
+
+ this.node_in_panel = null;
+ this.node_widget = null;
+
+ this.last_mouse = [0, 0];
+ this.last_mouseclick = 0;
+ this.pointer_is_down = false;
+ this.pointer_is_double = false;
+ this.visible_area.set([0, 0, 0, 0]);
+
+ if (this.onClear) {
+ this.onClear();
+ }
+ };
+
+ /**
+ * assigns a graph, you can reassign graphs to the same canvas
+ *
+ * @method setGraph
+ * @param {LGraph} graph
+ */
+ LGraphCanvas.prototype.setGraph = function(graph, skip_clear) {
+ if (this.graph == graph) {
+ return;
+ }
+
+ if (!skip_clear) {
+ this.clear();
+ }
+
+ if (!graph && this.graph) {
+ this.graph.detachCanvas(this);
+ return;
+ }
+
+ graph.attachCanvas(this);
+
+ //remove the graph stack in case a subgraph was open
+ if (this._graph_stack)
+ this._graph_stack = null;
+
+ this.setDirty(true, true);
+ };
+
+ /**
+ * returns the top level graph (in case there are subgraphs open on the canvas)
+ *
+ * @method getTopGraph
+ * @return {LGraph} graph
+ */
+ LGraphCanvas.prototype.getTopGraph = function()
+ {
+ if(this._graph_stack.length)
+ return this._graph_stack[0];
+ return this.graph;
+ }
+
+ /**
+ * opens a graph contained inside a node in the current graph
+ *
+ * @method openSubgraph
+ * @param {LGraph} graph
+ */
+ LGraphCanvas.prototype.openSubgraph = function(graph) {
+ if (!graph) {
+ throw "graph cannot be null";
+ }
+
+ if (this.graph == graph) {
+ throw "graph cannot be the same";
+ }
+
+ this.clear();
+
+ if (this.graph) {
+ if (!this._graph_stack) {
+ this._graph_stack = [];
+ }
+ this._graph_stack.push(this.graph);
+ }
+
+ graph.attachCanvas(this);
+ this.checkPanels();
+ this.setDirty(true, true);
+ };
+
+ /**
+ * closes a subgraph contained inside a node
+ *
+ * @method closeSubgraph
+ * @param {LGraph} assigns a graph
+ */
+ LGraphCanvas.prototype.closeSubgraph = function() {
+ if (!this._graph_stack || this._graph_stack.length == 0) {
+ return;
+ }
+ var subgraph_node = this.graph._subgraph_node;
+ var graph = this._graph_stack.pop();
+ this.selected_nodes = {};
+ this.highlighted_links = {};
+ graph.attachCanvas(this);
+ this.setDirty(true, true);
+ if (subgraph_node) {
+ this.centerOnNode(subgraph_node);
+ this.selectNodes([subgraph_node]);
+ }
+ // when close sub graph back to offset [0, 0] scale 1
+ this.ds.offset = [0, 0]
+ this.ds.scale = 1
+ };
+
+ /**
+ * returns the visually active graph (in case there are more in the stack)
+ * @method getCurrentGraph
+ * @return {LGraph} the active graph
+ */
+ LGraphCanvas.prototype.getCurrentGraph = function() {
+ return this.graph;
+ };
+
+ /**
+ * assigns a canvas
+ *
+ * @method setCanvas
+ * @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector)
+ */
+ LGraphCanvas.prototype.setCanvas = function(canvas, skip_events) {
+ var that = this;
+
+ if (canvas) {
+ if (canvas.constructor === String) {
+ canvas = document.getElementById(canvas);
+ if (!canvas) {
+ throw "Error creating LiteGraph canvas: Canvas not found";
+ }
+ }
+ }
+
+ if (canvas === this.canvas) {
+ return;
+ }
+
+ if (!canvas && this.canvas) {
+ //maybe detach events from old_canvas
+ if (!skip_events) {
+ this.unbindEvents();
+ }
+ }
+
+ this.canvas = canvas;
+ this.ds.element = canvas;
+
+ if (!canvas) {
+ return;
+ }
+
+ //this.canvas.tabindex = "1000";
+ canvas.className += " lgraphcanvas";
+ canvas.data = this;
+ canvas.tabindex = "1"; //to allow key events
+
+ //bg canvas: used for non changing stuff
+ this.bgcanvas = null;
+ if (!this.bgcanvas) {
+ this.bgcanvas = document.createElement("canvas");
+ this.bgcanvas.width = this.canvas.width;
+ this.bgcanvas.height = this.canvas.height;
+ }
+
+ if (canvas.getContext == null) {
+ if (canvas.localName != "canvas") {
+ throw "Element supplied for LGraphCanvas must be a