mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-06-21 15:29:26 +08:00
Compare commits
46 Commits
db4a167770
...
1f50823c36
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f50823c36 | ||
|
|
9ad16c0188 | ||
|
|
a2c41a2a21 | ||
|
|
9690eed4d1 | ||
|
|
5d1d287735 | ||
|
|
65789a6c9d | ||
|
|
871a646fd7 | ||
|
|
27f81a058e | ||
|
|
af11571dd7 | ||
|
|
f59dd4bdc6 | ||
|
|
90b87db05c | ||
|
|
d6a9d6544d | ||
|
|
42b028362e | ||
|
|
dcf04578b8 | ||
|
|
3eceec5bfa | ||
|
|
2c0d665929 | ||
|
|
7fa009e9da | ||
|
|
7cf155b2fe | ||
|
|
350ff2d044 | ||
|
|
f64b73fc57 | ||
|
|
1958957d36 | ||
|
|
f3236c3031 | ||
|
|
fcbad506bb | ||
|
|
5de92e4f92 | ||
|
|
8ea1cf7aa7 | ||
|
|
936dc09f22 | ||
|
|
5a19558c01 | ||
|
|
2513b3bff1 | ||
|
|
919c07f1e1 | ||
|
|
8f646e92a0 | ||
|
|
8d9eeb93f4 | ||
|
|
299976330f | ||
|
|
ea3dcde0ca | ||
|
|
5162e1d601 | ||
|
|
a129cc6321 | ||
|
|
1fc7b34f4a | ||
|
|
ba33ebe2dc | ||
|
|
d98ca19ef5 | ||
|
|
04e12f4b3b | ||
|
|
23056a0eb8 | ||
|
|
1bdcd1bdbf | ||
|
|
e04ed0eda7 | ||
|
|
02aa67b541 | ||
|
|
67d03530a3 | ||
|
|
6d87c3e981 | ||
|
|
acc11e1bda |
@ -4504,6 +4504,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "This is the rgb2x wrapper node for ComfyUI. The required models are automatically downloaded on the first run.\noriginal project : [a/https://github.com/zheng95z/rgbx](original project : https://github.com/zheng95z/rgbx)"
|
||||
},
|
||||
{
|
||||
"author": "flrngel",
|
||||
"title": "ComfyUI_rgbx_xrgb_Wrapper",
|
||||
"reference": "https://github.com/flrngel/ComfyUI_rgbx_xrgb_Wrapper",
|
||||
"files": [
|
||||
"https://github.com/flrngel/ComfyUI_rgbx_xrgb_Wrapper"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This is the rgb2x and x2rgb wrapper node for ComfyUI. The required models are automatically downloaded on the first run.\noriginal project : [a/https://github.com/zheng95z/rgbx](original project : https://github.com/zheng95z/rgbx)"
|
||||
},
|
||||
{
|
||||
"author": "thecooltechguy",
|
||||
"title": "ComfyUI Stable Video Diffusion",
|
||||
@ -17469,13 +17479,14 @@
|
||||
},
|
||||
{
|
||||
"author": "nicehero",
|
||||
"title": "comfyui-conditioning-saver",
|
||||
"title": "ComfyUI Conditioning Saver",
|
||||
"id": "conditioningsaver",
|
||||
"reference": "https://github.com/nicehero/comfyui-conditioning-saver",
|
||||
"files": [
|
||||
"https://github.com/nicehero/comfyui-conditioning-saver"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Save and load CONDITIONING tensors to/from files, enabling reuse of encoded prompts and caching expensive CLIP encodings across workflows."
|
||||
"description": "Save and load CONDITIONING tensors to or from files, enabling reuse of encoded prompts and caching expensive CLIP encodings across workflows."
|
||||
},
|
||||
{
|
||||
"author": "sakura1bgx",
|
||||
@ -31361,6 +31372,16 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "A custom node for ComfyUI that performs speaker diarization to isolate individual speaker audio tracks from a single audio source."
|
||||
},
|
||||
{
|
||||
"author": "pmarmotte2",
|
||||
"title": "Comfyui-Pick_Any",
|
||||
"reference": "https://github.com/pmarmotte2/Comfyui_Pick_Any",
|
||||
"files": [
|
||||
"https://github.com/pmarmotte2/Comfyui_Pick_Any"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Advanced chooser nodes for ComfyUI that let workflows pause for image, audio, or text selection, with optional automatic picking and series helper nodes."
|
||||
},
|
||||
{
|
||||
"author": "IIEleven11",
|
||||
"title": "ComfyUI-FairyTaler",
|
||||
@ -45473,6 +45494,17 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Workflow management and generation UI plugin. Browse, organize, and execute workflows with auto-generated parameter editing, AI prompt assistant (Ollama), JSON syntax highlighting, Eagle integration, and multi-language support (EN/JA/ZH)."
|
||||
},
|
||||
{
|
||||
"author": "ketle-man",
|
||||
"title": "Model and Prompt from Metadata",
|
||||
"id": "model-and-prompt-from-metadata",
|
||||
"reference": "https://github.com/ketle-man/model-and-prompt-from-metadata",
|
||||
"files": [
|
||||
"https://github.com/ketle-man/model-and-prompt-from-metadata"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Drop a ComfyUI PNG/WebP/JSON onto the node to instantly extract and apply checkpoint, VAE, and prompts from embedded metadata. Supports ComfyUI, SD WebUI, SD Forge neo, Fooocus, SDXLPromptStyler, and Lora Loader (LoraManager). Includes LoRA from Metadata and CLIP Text Encode edit+ nodes."
|
||||
},
|
||||
{
|
||||
"author": "senjinthedragon",
|
||||
"title": "ComfyUI Gender Tag Filter",
|
||||
@ -47462,6 +47494,89 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Simple ComfyUI node that determines the image size by a keyword in the prompt, optionally replacing the keyword"
|
||||
},
|
||||
{
|
||||
"author": "PaoloC68",
|
||||
"title": "ComfyUI-PuLID-Flux-Chroma",
|
||||
"id": "comfyui-pulid-flux-chroma",
|
||||
"reference": "https://github.com/PaoloC68/ComfyUI-PuLID-Flux-Chroma",
|
||||
"files": [
|
||||
"https://github.com/PaoloC68/ComfyUI-PuLID-Flux-Chroma"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "The first PuLID (face identity preservation) implementation with full Chroma model support for ComfyUI. Automatically detects and adapts to both FLUX and Chroma models. Fork of PuLID-Flux-Enhanced with comprehensive Chroma compatibility fixes.",
|
||||
"stars": 0,
|
||||
"last_update": "2025-06-15 00:00:00",
|
||||
"preemptions": [
|
||||
"ApplyPulidFlux",
|
||||
"PulidFluxModelLoader",
|
||||
"PulidFluxInsightFaceLoader",
|
||||
"PulidFluxEvaClipLoader"
|
||||
],
|
||||
"nodename_pattern": "PulidFlux",
|
||||
"badges": [
|
||||
"Chroma",
|
||||
"Face Identity"
|
||||
],
|
||||
"dependencies": [
|
||||
"insightface",
|
||||
"onnxruntime"
|
||||
],
|
||||
"pip": [
|
||||
"insightface",
|
||||
"onnxruntime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"author": "WJLUOXIAO",
|
||||
"title": "XB_ToolBox",
|
||||
"reference": "https://github.com/WJLUOXIAO/XB_ToolBox",
|
||||
"files": [
|
||||
"https://github.com/WJLUOXIAO/XB_ToolBox"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A comprehensive ComfyUI extension suite encompassing both front-end interaction and low-level memory scheduling, designed to help beginners master workflows and simplify local deployment."
|
||||
},
|
||||
{
|
||||
"author": "Anonymzx",
|
||||
"title": "BangtrixToolkit",
|
||||
"reference": "https://github.com/Anonymzx/BangtrixToolkit",
|
||||
"files": [
|
||||
"https://github.com/Anonymzx/BangtrixToolkit"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Advanced ComfyUI toolkit with translation, AMD ROCm utilities, optimization, and workflow enhance"
|
||||
},
|
||||
{
|
||||
"author": "Magos Digital Studio",
|
||||
"title": "ComfyUI-Magos-Nodes",
|
||||
"reference": "https://github.com/MagosDigitalStudio/ComfyUI-Magos-Nodes",
|
||||
"files": [
|
||||
"https://github.com/MagosDigitalStudio/ComfyUI-Magos-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "DWPose & NLF skeleton editor, retargeter, and renderer for ComfyUI. Extract body/hand/face keypoints (+ optional NLF 3D) from video, edit them frame-by-frame in a full-screen timeline + dope sheet + graph + 3D orbit editor with animated camera, retarget DWPose proportions per-cluster, and render clean pose images. Outputs standard ComfyUI types — drives WanAnimate, ControlNet, SCAIL, LTX-Video, UniAnimate, and any other pose-driven pipeline."
|
||||
},
|
||||
{
|
||||
"author": "PlagueKind",
|
||||
"title": "PlagueKind-Nodes",
|
||||
"reference": "https://github.com/PlagueKind/ComfyUI-PlagueKind-Nodes",
|
||||
"files": [
|
||||
"https://github.com/PlagueKind/ComfyUI-PlagueKind-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Custom ComfyUI nodes providing unified image and mask resizing with multiple scaling modes, aspect-ratio preservation, center crop alignment, and stable tensor-based mask transformations."
|
||||
},
|
||||
{
|
||||
"author": "dreamrec",
|
||||
"title": "ComfyUI-Pixal3D",
|
||||
"id": "comfyui-pixal3d",
|
||||
"reference": "https://github.com/dreamrec/ComfyUI-Pixal3D",
|
||||
"files": [
|
||||
"https://github.com/dreamrec/ComfyUI-Pixal3D"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI wrapper for [a/Pixal3D](https://github.com/TencentARC/Pixal3D) — Tencent's SIGGRAPH 2026 single-image to PBR-textured-3D pipeline. Windows + NVIDIA RTX 30/40/50 (≥ 16 GB VRAM). One image → textured PBR mesh in ~3-5 min. Built on top of [a/ComfyUI-Trellis2](https://github.com/visualbruno/ComfyUI-Trellis2). Note: Pixal3D itself is academic / non-commercial only and NOT for EU use — see NOTICE.md."
|
||||
},
|
||||
|
||||
|
||||
|
||||
@ -47854,4 +47969,4 @@
|
||||
"description": "This is a node to convert an image into a CMYK Halftone dot image."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -2956,6 +2956,14 @@
|
||||
"title_aux": "ComfyUI Qwen Prompt Expander"
|
||||
}
|
||||
],
|
||||
"https://github.com/Anonymzx/BangtrixToolkit": [
|
||||
[
|
||||
"BangtrixTranslateUniversal"
|
||||
],
|
||||
{
|
||||
"title_aux": "BangtrixToolkit"
|
||||
}
|
||||
],
|
||||
"https://github.com/Anzhc/Anima-Mod-Guidance-ComfyUI-Node": [
|
||||
[
|
||||
"AnimaModGuidance"
|
||||
@ -6779,7 +6787,8 @@
|
||||
"DenoLTXSequencer",
|
||||
"DenoMultiImageLoader",
|
||||
"DenoRTXVFXEasyUpscale",
|
||||
"DenoResolutionSetup"
|
||||
"DenoResolutionSetup",
|
||||
"DenoVideoCompare"
|
||||
],
|
||||
{
|
||||
"title_aux": "Deno Custom Nodes"
|
||||
@ -14863,6 +14872,19 @@
|
||||
"title_aux": "comfyui-vram-overlay"
|
||||
}
|
||||
],
|
||||
"https://github.com/MagosDigitalStudio/ComfyUI-Magos-Nodes": [
|
||||
[
|
||||
"DWPoseTEEditor",
|
||||
"DWPoseTEExtractor",
|
||||
"DWPoseTERenderer",
|
||||
"MagosPoseRetargeter",
|
||||
"WanAnimateSamplerPresets",
|
||||
"WanRatioAndFPS"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-Magos-Nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager": [
|
||||
[
|
||||
"MajoorSaveImage",
|
||||
@ -17478,6 +17500,24 @@
|
||||
"title_aux": "ComfyUI-Gallery"
|
||||
}
|
||||
],
|
||||
"https://github.com/PaoloC68/ComfyUI-PuLID-Flux-Chroma": [
|
||||
[
|
||||
"ApplyPulidFlux",
|
||||
"PulidFluxEvaClipLoader",
|
||||
"PulidFluxInsightFaceLoader",
|
||||
"PulidFluxModelLoader"
|
||||
],
|
||||
{
|
||||
"nodename_pattern": "PulidFlux",
|
||||
"preemptions": [
|
||||
"ApplyPulidFlux",
|
||||
"PulidFluxModelLoader",
|
||||
"PulidFluxInsightFaceLoader",
|
||||
"PulidFluxEvaClipLoader"
|
||||
],
|
||||
"title_aux": "ComfyUI-PuLID-Flux-Chroma"
|
||||
}
|
||||
],
|
||||
"https://github.com/Parameshvadivel/ComfyUI-SVGview": [
|
||||
[
|
||||
"SVGPreview"
|
||||
@ -17769,6 +17809,14 @@
|
||||
"title_aux": "Joy Caption Two - PixelaiLabs Edition"
|
||||
}
|
||||
],
|
||||
"https://github.com/PlagueKind/ComfyUI-PlagueKind-Nodes": [
|
||||
[
|
||||
"UnifiedResizeImageMask"
|
||||
],
|
||||
{
|
||||
"title_aux": "PlagueKind-Nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/PnthrLeo/comfyUI-PL-data-tools": [
|
||||
[
|
||||
"AreasGenerator",
|
||||
@ -18165,6 +18213,10 @@
|
||||
],
|
||||
"https://github.com/PozzettiAndrea/ComfyUI-TurntableGSViewer": [
|
||||
[
|
||||
"GaussianAnalysis",
|
||||
"GaussianExport",
|
||||
"GaussianMerge",
|
||||
"LoadPLY",
|
||||
"PreviewGaussians"
|
||||
],
|
||||
{
|
||||
@ -24083,6 +24135,35 @@
|
||||
"title_aux": "WAS Affine"
|
||||
}
|
||||
],
|
||||
"https://github.com/WJLUOXIAO/XB_ToolBox": [
|
||||
[
|
||||
"XB_BatchFolderLoader",
|
||||
"XB_CLIPNameBroadcaster",
|
||||
"XB_CheckpointBlockSwap",
|
||||
"XB_ChunkVisualization",
|
||||
"XB_Dashboard_Zen",
|
||||
"XB_DynamicBus",
|
||||
"XB_ImageParamsMaster",
|
||||
"XB_MasterParameter",
|
||||
"XB_SageAttentionAccelerator",
|
||||
"XB_SamplerChunkMaster",
|
||||
"XB_StoryboardSlicer",
|
||||
"XB_UNetBlockSwap",
|
||||
"XB_UNetNameBroadcaster",
|
||||
"XB_VRAM_Calculator",
|
||||
"XB_VideoParamsMaster",
|
||||
"XB_Video_Merger",
|
||||
"XB_WanFirstLastFrameToVideo",
|
||||
"XB_WanImageToVideo",
|
||||
"XB_Wan_ParamBus",
|
||||
"XB_Wan_RelayNode",
|
||||
"XTX_Data_Radar",
|
||||
"XTX_VRAM_Cleaner"
|
||||
],
|
||||
{
|
||||
"title_aux": "XB_ToolBox"
|
||||
}
|
||||
],
|
||||
"https://github.com/WUYUDING2583/ComfyUI-Save-Image-Callback": [
|
||||
[
|
||||
"Save Image With Callback"
|
||||
@ -30977,7 +31058,6 @@
|
||||
"IO_ImageSaveOverwrite",
|
||||
"IO_LoadAudioBatch",
|
||||
"IO_LoadImgBatch",
|
||||
"IO_LoadImgList",
|
||||
"IO_LoadShotBatch",
|
||||
"IO_LoadTextBatch",
|
||||
"IO_LoadVideoBatch",
|
||||
@ -31205,6 +31285,7 @@
|
||||
"mask_sam_detctor",
|
||||
"math_Remap_data",
|
||||
"math_calculate",
|
||||
"math_text_compare",
|
||||
"model_tool_assy",
|
||||
"pack_Pack",
|
||||
"pack_Unpack",
|
||||
@ -36177,6 +36258,16 @@
|
||||
"title_aux": "ComfyUI_show_seed"
|
||||
}
|
||||
],
|
||||
"https://github.com/dreamrec/ComfyUI-Pixal3D": [
|
||||
[
|
||||
"Pixal3DFreePipeline",
|
||||
"Pixal3DImageToMesh",
|
||||
"Pixal3DLoadPipeline"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-Pixal3D"
|
||||
}
|
||||
],
|
||||
"https://github.com/drmbt/comfyui-dreambait-nodes": [
|
||||
[
|
||||
"AudioInfoPlus",
|
||||
@ -38258,6 +38349,15 @@
|
||||
"title_aux": "ComfyUI-Flowty-TripoSR"
|
||||
}
|
||||
],
|
||||
"https://github.com/flrngel/ComfyUI_rgbx_xrgb_Wrapper": [
|
||||
[
|
||||
"rgb2x",
|
||||
"x2rgb"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI_rgbx_xrgb_Wrapper"
|
||||
}
|
||||
],
|
||||
"https://github.com/fluffydiveX/ComfyUI-hvBlockswap": [
|
||||
[
|
||||
"hvBlockSwap"
|
||||
@ -40444,6 +40544,7 @@
|
||||
"Double_Prompt_Encode",
|
||||
"FiveRandomLines",
|
||||
"Prompt_Extender",
|
||||
"Prompt_Library",
|
||||
"RandomLine",
|
||||
"Simple_Prompt_Library"
|
||||
],
|
||||
@ -44338,6 +44439,17 @@
|
||||
"title_aux": "ComfyUI VRM Pose Editor 3D"
|
||||
}
|
||||
],
|
||||
"https://github.com/ketle-man/model-and-prompt-from-metadata": [
|
||||
[
|
||||
"CLIPTextEncodeEditPlus",
|
||||
"ImageMetadataCheckpointLoader",
|
||||
"ImageMetadataLoRALoader",
|
||||
"ImageMetadataPromptLoader"
|
||||
],
|
||||
{
|
||||
"title_aux": "Model and Prompt from Metadata"
|
||||
}
|
||||
],
|
||||
"https://github.com/kevinmcmahondev/comfyui-kmcdev-image-filter-adjustments": [
|
||||
[
|
||||
"ImageBlankAlpha",
|
||||
@ -44768,6 +44880,7 @@
|
||||
"GetLatentSizeAndCount",
|
||||
"GetLatentsFromBatchIndexed",
|
||||
"GetMaskSizeAndCount",
|
||||
"GetPreviewOverrideFramesKJ",
|
||||
"GetTrackRange",
|
||||
"GradientToFloat",
|
||||
"GrowMaskWithBlur",
|
||||
@ -44813,7 +44926,6 @@
|
||||
"InsertImagesToBatchIndexed",
|
||||
"InsertLatentToIndexed",
|
||||
"InterpolateCoords",
|
||||
"Intrinsic_lora_sampling",
|
||||
"JoinStringMulti",
|
||||
"JoinStrings",
|
||||
"LTX2AttentionTunerPatch",
|
||||
@ -44844,6 +44956,7 @@
|
||||
"ModelMemoryUseReportPatch",
|
||||
"ModelPassThrough",
|
||||
"ModelPatchTorchSettings",
|
||||
"ModelPreviewOverrideKJ",
|
||||
"ModelSaveKJ",
|
||||
"NABLA_AttentionKJ",
|
||||
"NormalizedAmplitudeToFloatList",
|
||||
@ -51455,7 +51568,7 @@
|
||||
"SaveConditioning"
|
||||
],
|
||||
{
|
||||
"title_aux": "comfyui-conditioning-saver"
|
||||
"title_aux": "ComfyUI Conditioning Saver"
|
||||
}
|
||||
],
|
||||
"https://github.com/nickve28/ComfyUI-Nich-Utils": [
|
||||
@ -52440,6 +52553,7 @@
|
||||
"FunPackAdvisorLLM",
|
||||
"FunPackApplyLoraWeights",
|
||||
"FunPackClipVisionOutputCombine",
|
||||
"FunPackConditioningAdjust",
|
||||
"FunPackContextTransitionWindows",
|
||||
"FunPackContinueVideo",
|
||||
"FunPackDistilledFlowSampler",
|
||||
@ -52453,6 +52567,7 @@
|
||||
"FunPackStoryMemKeyframeExtractor",
|
||||
"FunPackStoryMemLastFrameExtractor",
|
||||
"FunPackStoryWriter",
|
||||
"FunPackStudio",
|
||||
"FunPackVideoRefinerV2",
|
||||
"FunPackVideoStitch"
|
||||
],
|
||||
@ -53666,6 +53781,19 @@
|
||||
"title_aux": "ComfyUI-Speaker-Isolation"
|
||||
}
|
||||
],
|
||||
"https://github.com/pmarmotte2/Comfyui_Pick_Any": [
|
||||
[
|
||||
"ADVAudioSeries",
|
||||
"ADVAudioSeries4",
|
||||
"ADVImageSeries4",
|
||||
"ADVPickAnyChooser",
|
||||
"ADVTextSeries",
|
||||
"ADVTextSeries4"
|
||||
],
|
||||
{
|
||||
"title_aux": "Comfyui-Pick_Any"
|
||||
}
|
||||
],
|
||||
"https://github.com/pnikolic-amd/ComfyUI_MIGraphX": [
|
||||
[
|
||||
"CompileDiffusersMIGraphX"
|
||||
@ -55328,9 +55456,11 @@
|
||||
[
|
||||
"RikanHiddenBase64ImageLoader",
|
||||
"RikanHiddenBase64ImageSaver",
|
||||
"RikanMultiLoraLoader",
|
||||
"RikanPromptRelayEncodeTimeline",
|
||||
"RikanPromptRelayMultiLoraGate",
|
||||
"RikanQwenCustomImageSize",
|
||||
"RikanRTXResolutionSettings",
|
||||
"RikanWanSpatioTemporalTiledVAEDecode",
|
||||
"rikan-i2vpainter",
|
||||
"rikan-i2vpainter-tiled-vae"
|
||||
|
||||
8365
github-stats.json
8365
github-stats.json
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,10 @@ import threading
|
||||
import re
|
||||
import shutil
|
||||
import git
|
||||
import glob
|
||||
import json
|
||||
from datetime import datetime
|
||||
from contextlib import contextmanager
|
||||
|
||||
from server import PromptServer
|
||||
import manager_core as core
|
||||
@ -816,6 +819,86 @@ async def fetch_updates(request):
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return web.Response(status=400)
|
||||
|
||||
@routes.get("/customnode/get_node_types_in_workflows")
|
||||
async def get_node_types_in_workflows(request):
|
||||
try:
|
||||
# get our username from the request header
|
||||
user_id = PromptServer.instance.user_manager.get_request_user_id(request)
|
||||
|
||||
# get the base workflow directory (TODO: figure out if non-standard directories are possible, and how to find them)
|
||||
workflow_files_base_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), user_id, "workflows"))
|
||||
|
||||
logging.debug(f"workflows base path: {workflow_files_base_path}")
|
||||
|
||||
# workflow directory doesn't actually exist, return 204 (No Content)
|
||||
if not os.path.isdir(workflow_files_base_path):
|
||||
logging.debug("workflows base path doesn't exist - nothing to do...")
|
||||
return web.Response(status=204)
|
||||
|
||||
# get all JSON files under the workflow directory
|
||||
workflow_file_relative_paths: list[str] = glob.glob(pathname="**/*.json", root_dir=workflow_files_base_path, recursive=True)
|
||||
|
||||
logging.debug(f"found the following workflows: {workflow_file_relative_paths}")
|
||||
|
||||
# set up our list of workflow/node-lists
|
||||
workflow_node_mappings: list[dict[str, str | list[str]]] = []
|
||||
|
||||
# iterate over each found JSON file
|
||||
for workflow_file_path in workflow_file_relative_paths:
|
||||
|
||||
try:
|
||||
workflow_file_absolute_path = os.path.abspath(os.path.join(workflow_files_base_path, workflow_file_path))
|
||||
logging.debug(f"starting work on {workflow_file_absolute_path}")
|
||||
# load the JSON file
|
||||
workflow_file_data = json.load(open(workflow_file_absolute_path, "r"))
|
||||
|
||||
# make sure there's a nodes key (otherwise this might not actually be a workflow file)
|
||||
if "nodes" not in workflow_file_data:
|
||||
logging.warning(f"{workflow_file_path} has no 'nodes' key (possibly invalid?) - skipping...")
|
||||
# skip to next file
|
||||
continue
|
||||
|
||||
# now this looks like a valid file, so let's get to work
|
||||
new_mapping = {"workflow_file_name": workflow_file_path}
|
||||
# we can't use an actual set, because you can't use dicts as set members
|
||||
node_set = []
|
||||
|
||||
# iterate over each node in the workflow
|
||||
for node in workflow_file_data["nodes"]:
|
||||
if "id" not in node:
|
||||
logging.warning(f"Found a node with no ID - possibly corrupt/invalid workflow?")
|
||||
continue
|
||||
# if there's no type, throw a warning
|
||||
if "type" not in node:
|
||||
logging.warning(f"Node type not found in {workflow_file_path} for node ID {node['id']}")
|
||||
# skip to next node
|
||||
continue
|
||||
|
||||
node_data_to_return = {"type": node["type"]}
|
||||
if "properties" not in node:
|
||||
logging.warning(f"Node ${node['id']} has no properties field - can't determine cnr_id")
|
||||
else:
|
||||
for property_key in ["cnr_id", "ver"]:
|
||||
if property_key in node["properties"]:
|
||||
node_data_to_return[property_key] = node["properties"][property_key]
|
||||
|
||||
# add it to the list for this workflow
|
||||
if not node_data_to_return in node_set:
|
||||
node_set.append(node_data_to_return)
|
||||
|
||||
# annoyingly, Python can't serialize sets to JSON
|
||||
new_mapping["node_types"] = list(node_set)
|
||||
workflow_node_mappings.append(new_mapping)
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Couldn't open {workflow_file_path}: {e}")
|
||||
|
||||
return web.json_response(workflow_node_mappings, content_type='application/json')
|
||||
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return web.Response(status=500)
|
||||
|
||||
|
||||
@routes.post("/manager/queue/update_all")
|
||||
|
||||
@ -9,6 +9,7 @@ This directory contains the JavaScript frontend implementation for ComfyUI-Manag
|
||||
- **model-manager.js**: Handles the model management interface for downloading and organizing AI models.
|
||||
- **components-manager.js**: Manages reusable workflow components system.
|
||||
- **snapshot.js**: Implements the snapshot system for backing up and restoring installations.
|
||||
- **node-usage-analyzer.js**: Implements the UI for analyzing node usage in workflows.
|
||||
|
||||
## Sharing Components
|
||||
|
||||
@ -46,5 +47,6 @@ The frontend follows a modular component-based architecture:
|
||||
CSS files are included for specific components:
|
||||
- **custom-nodes-manager.css**: Styling for the node management UI
|
||||
- **model-manager.css**: Styling for the model management UI
|
||||
- **node-usage-analyzer.css**: Styling for the node usage analyzer UI
|
||||
|
||||
This frontend implementation provides a comprehensive yet user-friendly interface for managing the ComfyUI ecosystem.
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
} from "./common.js";
|
||||
import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js";
|
||||
import { CustomNodesManager } from "./custom-nodes-manager.js";
|
||||
import { NodeUsageAnalyzer } from "./node-usage-analyzer.js";
|
||||
import { ModelManager } from "./model-manager.js";
|
||||
import { SnapshotManager } from "./snapshot.js";
|
||||
import { buildGuiFrame, createSettingsCombo } from "./comfyui-gui-builder.js";
|
||||
@ -909,6 +910,17 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
CustomNodesManager.instance.show(CustomNodesManager.ShowMode.IN_WORKFLOW);
|
||||
}
|
||||
}),
|
||||
$el("button.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Node Usage Analyzer",
|
||||
onclick:
|
||||
() => {
|
||||
if(!NodeUsageAnalyzer.instance) {
|
||||
NodeUsageAnalyzer.instance = new NodeUsageAnalyzer(app, self);
|
||||
}
|
||||
NodeUsageAnalyzer.instance.show(NodeUsageAnalyzer.SortMode.BY_PACKAGE);
|
||||
}
|
||||
}),
|
||||
|
||||
$el("div", {}, []),
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
|
||||
457
js/common.js
457
js/common.js
@ -122,9 +122,9 @@ export async function customConfirm(message) {
|
||||
let res = await
|
||||
window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Confirm',
|
||||
message: message
|
||||
});
|
||||
title: 'Confirm',
|
||||
message: message
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
@ -164,9 +164,9 @@ export async function customPrompt(title, message) {
|
||||
let res = await
|
||||
window['app'].extensionManager.dialog
|
||||
.prompt({
|
||||
title: title,
|
||||
message: message
|
||||
});
|
||||
title: title,
|
||||
message: message
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
@ -667,4 +667,449 @@ function initTooltip () {
|
||||
document.body.addEventListener('mouseleave', mouseleaveHandler, true);
|
||||
}
|
||||
|
||||
export async function uninstallNodes(nodeList, options = {}) {
|
||||
const {
|
||||
title = `${nodeList.length} custom nodes`,
|
||||
onProgress = () => {},
|
||||
onError = () => {},
|
||||
onSuccess = () => {},
|
||||
channel = 'default',
|
||||
mode = 'default'
|
||||
} = options;
|
||||
|
||||
// Check if queue is busy
|
||||
let stats = await api.fetchApi('/manager/queue/status');
|
||||
stats = await stats.json();
|
||||
if (stats.is_processing) {
|
||||
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
|
||||
return { success: false, error: 'Queue is busy' };
|
||||
}
|
||||
|
||||
// Confirmation dialog for uninstall
|
||||
const confirmed = await customConfirm(`Are you sure uninstall ${title}?`);
|
||||
if (!confirmed) {
|
||||
return { success: false, error: 'User cancelled' };
|
||||
}
|
||||
|
||||
let errorMsg = "";
|
||||
let target_items = [];
|
||||
|
||||
await api.fetchApi('/manager/queue/reset');
|
||||
|
||||
for (const nodeItem of nodeList) {
|
||||
target_items.push(nodeItem);
|
||||
|
||||
onProgress(`Uninstall ${nodeItem.title || nodeItem.name} ...`);
|
||||
|
||||
const data = nodeItem.originalData || nodeItem;
|
||||
data.channel = channel;
|
||||
data.mode = mode;
|
||||
data.ui_id = nodeItem.hash || md5(nodeItem.name || nodeItem.title);
|
||||
|
||||
const res = await api.fetchApi(`/manager/queue/uninstall`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (res.status != 200) {
|
||||
errorMsg = `'${nodeItem.title || nodeItem.name}': `;
|
||||
|
||||
if (res.status == 403) {
|
||||
errorMsg += `This action is not allowed with this security level configuration.\n`;
|
||||
} else if (res.status == 404) {
|
||||
errorMsg += `With the current security level configuration, only custom nodes from the <B>"default channel"</B> can be uninstalled.\n`;
|
||||
} else {
|
||||
errorMsg += await res.text() + '\n';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
onError(errorMsg);
|
||||
show_message("[Uninstall Errors]\n" + errorMsg);
|
||||
return { success: false, error: errorMsg, targets: target_items };
|
||||
} else {
|
||||
await api.fetchApi('/manager/queue/start');
|
||||
onSuccess(target_items);
|
||||
showTerminal();
|
||||
return { success: true, targets: target_items };
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================================
|
||||
// Workflow Utilities Consolidation
|
||||
|
||||
export async function getWorkflowNodeTypes() {
|
||||
try {
|
||||
const res = await fetchData('/customnode/get_node_types_in_workflows');
|
||||
|
||||
if (res.status === 200) {
|
||||
return { success: true, data: res.data };
|
||||
} else if (res.status === 204) {
|
||||
// No workflows found - return empty list
|
||||
return { success: true, data: [] };
|
||||
} else {
|
||||
return { success: false, error: res.error };
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error };
|
||||
}
|
||||
}
|
||||
|
||||
export function findPackageByCnrId(cnrId, nodePackages, installedOnly = true) {
|
||||
if (!cnrId || !nodePackages) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Tier 1: Direct key match
|
||||
if (nodePackages[cnrId]) {
|
||||
const pack = nodePackages[cnrId];
|
||||
if (!installedOnly || pack.state !== "not-installed") {
|
||||
return { key: cnrId, pack: pack };
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 2: Case-insensitive match
|
||||
const cnrIdLower = cnrId.toLowerCase();
|
||||
for (const packKey of Object.keys(nodePackages)) {
|
||||
if (packKey.toLowerCase() === cnrIdLower) {
|
||||
const pack = nodePackages[packKey];
|
||||
if (!installedOnly || pack.state !== "not-installed") {
|
||||
return { key: packKey, pack: pack };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 3: URL/reference contains match
|
||||
for (const packKey of Object.keys(nodePackages)) {
|
||||
const pack = nodePackages[packKey];
|
||||
|
||||
// Skip non-installed packages if installedOnly is true
|
||||
if (installedOnly && pack.state === "not-installed") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if reference URL contains cnr_id
|
||||
if (pack.reference && pack.reference.includes(cnrId)) {
|
||||
return { key: packKey, pack: pack };
|
||||
}
|
||||
|
||||
// Check if any file URL contains cnr_id
|
||||
if (pack.files && Array.isArray(pack.files)) {
|
||||
for (const fileUrl of pack.files) {
|
||||
if (fileUrl.includes(cnrId)) {
|
||||
return { key: packKey, pack: pack };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function analyzeWorkflowUsage(nodePackages) {
|
||||
const result = await getWorkflowNodeTypes();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
|
||||
const workflowNodeList = result.data;
|
||||
const usageMap = new Map();
|
||||
const workflowDetailsMap = new Map();
|
||||
|
||||
if (workflowNodeList && Array.isArray(workflowNodeList)) {
|
||||
const cnrIdCounts = new Map();
|
||||
const cnrIdToWorkflows = new Map();
|
||||
|
||||
// Process each workflow
|
||||
workflowNodeList.forEach((workflowObj, workflowIndex) => {
|
||||
if (workflowObj.node_types && Array.isArray(workflowObj.node_types)) {
|
||||
const workflowCnrIds = new Set();
|
||||
|
||||
// Get workflow filename
|
||||
const workflowFilename = workflowObj.workflow_file_name ||
|
||||
workflowObj.filename ||
|
||||
workflowObj.file ||
|
||||
workflowObj.name ||
|
||||
workflowObj.path ||
|
||||
`Workflow ${workflowIndex + 1}`;
|
||||
|
||||
// Count nodes per cnr_id in this workflow
|
||||
const workflowCnrIdCounts = new Map();
|
||||
workflowObj.node_types.forEach(nodeTypeObj => {
|
||||
const cnrId = nodeTypeObj.cnr_id;
|
||||
|
||||
if (cnrId && cnrId !== "comfy-core") {
|
||||
// Track unique cnr_ids per workflow
|
||||
workflowCnrIds.add(cnrId);
|
||||
|
||||
// Count nodes per cnr_id in this specific workflow
|
||||
const workflowNodeCount = workflowCnrIdCounts.get(cnrId) || 0;
|
||||
workflowCnrIdCounts.set(cnrId, workflowNodeCount + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Record workflow details for each unique cnr_id found in this workflow
|
||||
workflowCnrIds.forEach(cnrId => {
|
||||
// Count occurrences of this cnr_id across all workflows
|
||||
const currentCount = cnrIdCounts.get(cnrId) || 0;
|
||||
cnrIdCounts.set(cnrId, currentCount + 1);
|
||||
|
||||
// Track workflow details
|
||||
if (!cnrIdToWorkflows.has(cnrId)) {
|
||||
cnrIdToWorkflows.set(cnrId, []);
|
||||
}
|
||||
cnrIdToWorkflows.get(cnrId).push({
|
||||
filename: workflowFilename,
|
||||
nodeCount: workflowCnrIdCounts.get(cnrId) || 0
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Map cnr_id to installed packages with workflow details
|
||||
cnrIdCounts.forEach((count, cnrId) => {
|
||||
const workflowDetails = cnrIdToWorkflows.get(cnrId) || [];
|
||||
|
||||
const foundPackage = findPackageByCnrId(cnrId, nodePackages, true);
|
||||
if (foundPackage) {
|
||||
usageMap.set(foundPackage.key, count);
|
||||
workflowDetailsMap.set(foundPackage.key, workflowDetails);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
usageMap: usageMap,
|
||||
workflowDetailsMap: workflowDetailsMap
|
||||
};
|
||||
}
|
||||
|
||||
// Size formatting utilities - consolidated from model-manager.js and node-usage-analyzer.js
|
||||
export function formatSize(v) {
|
||||
const base = 1000;
|
||||
const units = ['', 'K', 'M', 'G', 'T', 'P'];
|
||||
const space = '';
|
||||
const postfix = 'B';
|
||||
if (v <= 0) {
|
||||
return `0${space}${postfix}`;
|
||||
}
|
||||
for (let i = 0, l = units.length; i < l; i++) {
|
||||
const min = Math.pow(base, i);
|
||||
const max = Math.pow(base, i + 1);
|
||||
if (v > min && v <= max) {
|
||||
const unit = units[i];
|
||||
if (unit) {
|
||||
const n = v / min;
|
||||
const nl = n.toString().split('.')[0].length;
|
||||
const fl = Math.max(3 - nl, 1);
|
||||
v = n.toFixed(fl);
|
||||
}
|
||||
v = v + space + unit + postfix;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// for size sort
|
||||
export function sizeToBytes(v) {
|
||||
if (typeof v === "number") {
|
||||
return v;
|
||||
}
|
||||
if (typeof v === "string") {
|
||||
const n = parseFloat(v);
|
||||
const unit = v.replace(/[0-9.B]+/g, "").trim().toUpperCase();
|
||||
if (unit === "K") {
|
||||
return n * 1000;
|
||||
}
|
||||
if (unit === "M") {
|
||||
return n * 1000 * 1000;
|
||||
}
|
||||
if (unit === "G") {
|
||||
return n * 1000 * 1000 * 1000;
|
||||
}
|
||||
if (unit === "T") {
|
||||
return n * 1000 * 1000 * 1000 * 1000;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Flyover component - consolidated from custom-nodes-manager.js and node-usage-analyzer.js
|
||||
export function createFlyover(container, options = {}) {
|
||||
const {
|
||||
enableHover = false,
|
||||
hoverHandler = null,
|
||||
context = null
|
||||
} = options;
|
||||
|
||||
const $flyover = document.createElement("div");
|
||||
$flyover.className = "cn-flyover";
|
||||
$flyover.innerHTML = `<div class="cn-flyover-header">
|
||||
<div class="cn-flyover-close">${icons.arrowRight}</div>
|
||||
<div class="cn-flyover-title"></div>
|
||||
<div class="cn-flyover-close">${icons.close}</div>
|
||||
</div>
|
||||
<div class="cn-flyover-body"></div>`
|
||||
container.appendChild($flyover);
|
||||
|
||||
const $flyoverTitle = $flyover.querySelector(".cn-flyover-title");
|
||||
const $flyoverBody = $flyover.querySelector(".cn-flyover-body");
|
||||
|
||||
let width = '50%';
|
||||
let visible = false;
|
||||
|
||||
let timeHide;
|
||||
const closeHandler = (e) => {
|
||||
if ($flyover === e.target || $flyover.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timeHide);
|
||||
timeHide = setTimeout(() => {
|
||||
flyover.hide();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
const displayHandler = () => {
|
||||
if (visible) {
|
||||
$flyover.classList.remove("cn-slide-in-right");
|
||||
} else {
|
||||
$flyover.classList.remove("cn-slide-out-right");
|
||||
$flyover.style.width = '0px';
|
||||
$flyover.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
const flyover = {
|
||||
show: (titleHtml, bodyHtml) => {
|
||||
clearTimeout(timeHide);
|
||||
if (context && context.element) {
|
||||
context.element.removeEventListener("click", closeHandler);
|
||||
}
|
||||
$flyoverTitle.innerHTML = titleHtml;
|
||||
$flyoverBody.innerHTML = bodyHtml;
|
||||
$flyover.style.display = "block";
|
||||
$flyover.style.width = width;
|
||||
if(!visible) {
|
||||
$flyover.classList.add("cn-slide-in-right");
|
||||
}
|
||||
visible = true;
|
||||
setTimeout(() => {
|
||||
if (context && context.element) {
|
||||
context.element.addEventListener("click", closeHandler);
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
hide: (now) => {
|
||||
visible = false;
|
||||
if (context && context.element) {
|
||||
context.element.removeEventListener("click", closeHandler);
|
||||
}
|
||||
if(now) {
|
||||
displayHandler();
|
||||
return;
|
||||
}
|
||||
$flyover.classList.add("cn-slide-out-right");
|
||||
}
|
||||
}
|
||||
|
||||
$flyover.addEventListener("animationend", (e) => {
|
||||
displayHandler();
|
||||
});
|
||||
|
||||
// Add hover handlers if enabled
|
||||
if (enableHover && hoverHandler) {
|
||||
$flyover.addEventListener("mouseenter", hoverHandler, true);
|
||||
$flyover.addEventListener("mouseleave", hoverHandler, true);
|
||||
}
|
||||
|
||||
$flyover.addEventListener("click", (e) => {
|
||||
if(e.target.classList.contains("cn-flyover-close")) {
|
||||
flyover.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward other click events to the provided handler or context
|
||||
if (context && context.handleFlyoverClick) {
|
||||
context.handleFlyoverClick(e);
|
||||
}
|
||||
});
|
||||
|
||||
return flyover;
|
||||
}
|
||||
|
||||
// Shared UI State Methods - consolidated from multiple managers
|
||||
export function createUIStateManager(element, selectors) {
|
||||
return {
|
||||
showSelection: (msg) => {
|
||||
const el = element.querySelector(selectors.selection);
|
||||
if (el) el.innerHTML = msg;
|
||||
},
|
||||
|
||||
showError: (err) => {
|
||||
const el = element.querySelector(selectors.message);
|
||||
if (el) {
|
||||
const msg = err ? `<font color="red">${err}</font>` : "";
|
||||
el.innerHTML = msg;
|
||||
}
|
||||
},
|
||||
|
||||
showMessage: (msg, color) => {
|
||||
const el = element.querySelector(selectors.message);
|
||||
if (el) {
|
||||
if (color) {
|
||||
msg = `<font color="${color}">${msg}</font>`;
|
||||
}
|
||||
el.innerHTML = msg;
|
||||
}
|
||||
},
|
||||
|
||||
showStatus: (msg, color) => {
|
||||
const el = element.querySelector(selectors.status);
|
||||
if (el) {
|
||||
if (color) {
|
||||
msg = `<font color="${color}">${msg}</font>`;
|
||||
}
|
||||
el.innerHTML = msg;
|
||||
}
|
||||
},
|
||||
|
||||
showLoading: (grid) => {
|
||||
if (grid) {
|
||||
grid.showLoading();
|
||||
grid.showMask({
|
||||
opacity: 0.05
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
hideLoading: (grid) => {
|
||||
if (grid) {
|
||||
grid.hideLoading();
|
||||
grid.hideMask();
|
||||
}
|
||||
},
|
||||
|
||||
showRefresh: () => {
|
||||
const el = element.querySelector(selectors.refresh);
|
||||
if (el) el.style.display = "block";
|
||||
},
|
||||
|
||||
showStop: () => {
|
||||
const el = element.querySelector(selectors.stop);
|
||||
if (el) el.style.display = "block";
|
||||
},
|
||||
|
||||
hideStop: () => {
|
||||
const el = element.querySelector(selectors.stop);
|
||||
if (el) el.style.display = "none";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initTooltip();
|
||||
@ -8,7 +8,7 @@ import {
|
||||
fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt,
|
||||
sanitizeHTML, infoToast, showTerminal, setNeedRestart,
|
||||
storeColumnWidth, restoreColumnWidth, getTimeAgo, copyText, loadCss,
|
||||
showPopover, hidePopover, handle403Response
|
||||
showPopover, hidePopover, getWorkflowNodeTypes, findPackageByCnrId, analyzeWorkflowUsage, createFlyover
|
||||
} from "./common.js";
|
||||
|
||||
// https://cenfun.github.io/turbogrid/api.html
|
||||
@ -42,6 +42,8 @@ const ShowMode = {
|
||||
FAVORITES: "Favorites",
|
||||
ALTERNATIVES: "Alternatives",
|
||||
IN_WORKFLOW: "In Workflow",
|
||||
USED_IN_ANY_WORKFLOW: "Used In Any Workflow",
|
||||
NOT_USED_IN_ANY_WORKFLOW: "Installed and Unused",
|
||||
};
|
||||
|
||||
export class CustomNodesManager {
|
||||
@ -271,6 +273,14 @@ export class CustomNodesManager {
|
||||
label: "In Workflow",
|
||||
value: ShowMode.IN_WORKFLOW,
|
||||
hasData: false
|
||||
}, {
|
||||
label: "Used In Any Workflow",
|
||||
value: ShowMode.USED_IN_ANY_WORKFLOW,
|
||||
hasData: false
|
||||
}, {
|
||||
label: "Installed and Unused",
|
||||
value: ShowMode.NOT_USED_IN_ANY_WORKFLOW,
|
||||
hasData: false
|
||||
}, {
|
||||
label: "Missing",
|
||||
value: ShowMode.MISSING,
|
||||
@ -521,7 +531,11 @@ export class CustomNodesManager {
|
||||
const grid = new TG.Grid(container);
|
||||
this.grid = grid;
|
||||
|
||||
this.flyover = this.createFlyover(container);
|
||||
this.flyover = createFlyover(container, {
|
||||
enableHover: true,
|
||||
hoverHandler: this.handleFlyoverHover.bind(this),
|
||||
context: this
|
||||
});
|
||||
|
||||
let prevViewRowsLength = -1;
|
||||
grid.bind('onUpdated', (e, d) => {
|
||||
@ -1063,143 +1077,63 @@ export class CustomNodesManager {
|
||||
hidePopover();
|
||||
}
|
||||
|
||||
createFlyover(container) {
|
||||
const $flyover = document.createElement("div");
|
||||
$flyover.className = "cn-flyover";
|
||||
$flyover.innerHTML = `<div class="cn-flyover-header">
|
||||
<div class="cn-flyover-close">${icons.arrowRight}</div>
|
||||
<div class="cn-flyover-title"></div>
|
||||
<div class="cn-flyover-close">${icons.close}</div>
|
||||
</div>
|
||||
<div class="cn-flyover-body"></div>`
|
||||
container.appendChild($flyover);
|
||||
|
||||
const $flyoverTitle = $flyover.querySelector(".cn-flyover-title");
|
||||
const $flyoverBody = $flyover.querySelector(".cn-flyover-body");
|
||||
|
||||
let width = '50%';
|
||||
let visible = false;
|
||||
|
||||
let timeHide;
|
||||
const closeHandler = (e) => {
|
||||
if ($flyover === e.target || $flyover.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timeHide);
|
||||
timeHide = setTimeout(() => {
|
||||
flyover.hide();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
const hoverHandler = (e) => {
|
||||
if(e.type === "mouseenter") {
|
||||
if(e.target.classList.contains("cn-nodes-name")) {
|
||||
this.showNodePreview(e.target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.hideNodePreview();
|
||||
}
|
||||
|
||||
const displayHandler = () => {
|
||||
if (visible) {
|
||||
$flyover.classList.remove("cn-slide-in-right");
|
||||
} else {
|
||||
$flyover.classList.remove("cn-slide-out-right");
|
||||
$flyover.style.width = '0px';
|
||||
$flyover.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
const flyover = {
|
||||
show: (titleHtml, bodyHtml) => {
|
||||
clearTimeout(timeHide);
|
||||
this.element.removeEventListener("click", closeHandler);
|
||||
$flyoverTitle.innerHTML = titleHtml;
|
||||
$flyoverBody.innerHTML = bodyHtml;
|
||||
$flyover.style.display = "block";
|
||||
$flyover.style.width = width;
|
||||
if(!visible) {
|
||||
$flyover.classList.add("cn-slide-in-right");
|
||||
}
|
||||
visible = true;
|
||||
setTimeout(() => {
|
||||
this.element.addEventListener("click", closeHandler);
|
||||
}, 100);
|
||||
},
|
||||
hide: (now) => {
|
||||
visible = false;
|
||||
this.element.removeEventListener("click", closeHandler);
|
||||
if(now) {
|
||||
displayHandler();
|
||||
return;
|
||||
}
|
||||
$flyover.classList.add("cn-slide-out-right");
|
||||
}
|
||||
}
|
||||
|
||||
$flyover.addEventListener("animationend", (e) => {
|
||||
displayHandler();
|
||||
});
|
||||
|
||||
$flyover.addEventListener("mouseenter", hoverHandler, true);
|
||||
$flyover.addEventListener("mouseleave", hoverHandler, true);
|
||||
|
||||
$flyover.addEventListener("click", (e) => {
|
||||
|
||||
handleFlyoverHover(e) {
|
||||
if(e.type === "mouseenter") {
|
||||
if(e.target.classList.contains("cn-nodes-name")) {
|
||||
const nodeName = e.target.innerText;
|
||||
const nodeItem = this.nodeMap[nodeName];
|
||||
if (!nodeItem) {
|
||||
copyText(nodeName).then((res) => {
|
||||
if (res) {
|
||||
e.target.setAttribute("action", "Copied");
|
||||
e.target.classList.add("action");
|
||||
setTimeout(() => {
|
||||
e.target.classList.remove("action");
|
||||
e.target.removeAttribute("action");
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.showNodePreview(e.target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.hideNodePreview();
|
||||
}
|
||||
|
||||
const [x, y, w, h] = app.canvas.ds.visible_area;
|
||||
const dpi = Math.max(window.devicePixelRatio ?? 1, 1);
|
||||
const node = window.LiteGraph?.createNode(
|
||||
nodeItem.name,
|
||||
nodeItem.display_name,
|
||||
{
|
||||
pos: [x + (w-300) / dpi / 2, y]
|
||||
handleFlyoverClick(e) {
|
||||
if(e.target.classList.contains("cn-nodes-name")) {
|
||||
const nodeName = e.target.innerText;
|
||||
const nodeItem = this.nodeMap[nodeName];
|
||||
if (!nodeItem) {
|
||||
copyText(nodeName).then((res) => {
|
||||
if (res) {
|
||||
e.target.setAttribute("action", "Copied");
|
||||
e.target.classList.add("action");
|
||||
setTimeout(() => {
|
||||
e.target.classList.remove("action");
|
||||
e.target.removeAttribute("action");
|
||||
}, 1000);
|
||||
}
|
||||
);
|
||||
if (node) {
|
||||
app.graph.add(node);
|
||||
e.target.setAttribute("action", "Added to Workflow");
|
||||
e.target.classList.add("action");
|
||||
setTimeout(() => {
|
||||
e.target.classList.remove("action");
|
||||
e.target.removeAttribute("action");
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(e.target.classList.contains("cn-nodes-pack")) {
|
||||
const hash = e.target.getAttribute("hash");
|
||||
const rowItem = this.grid.getRowItemBy("hash", hash);
|
||||
//console.log(rowItem);
|
||||
this.grid.scrollToRow(rowItem);
|
||||
this.addHighlight(rowItem);
|
||||
return;
|
||||
}
|
||||
if(e.target.classList.contains("cn-flyover-close")) {
|
||||
flyover.hide();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return flyover;
|
||||
const [x, y, w, h] = app.canvas.ds.visible_area;
|
||||
const dpi = Math.max(window.devicePixelRatio ?? 1, 1);
|
||||
const node = window.LiteGraph?.createNode(
|
||||
nodeItem.name,
|
||||
nodeItem.display_name,
|
||||
{
|
||||
pos: [x + (w-300) / dpi / 2, y]
|
||||
}
|
||||
);
|
||||
if (node) {
|
||||
app.graph.add(node);
|
||||
e.target.setAttribute("action", "Added to Workflow");
|
||||
e.target.classList.add("action");
|
||||
setTimeout(() => {
|
||||
e.target.classList.remove("action");
|
||||
e.target.removeAttribute("action");
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if(e.target.classList.contains("cn-nodes-pack")) {
|
||||
const hash = e.target.getAttribute("hash");
|
||||
const rowItem = this.grid.getRowItemBy("hash", hash);
|
||||
//console.log(rowItem);
|
||||
this.grid.scrollToRow(rowItem);
|
||||
this.addHighlight(rowItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showNodes(d) {
|
||||
@ -1918,7 +1852,10 @@ export class CustomNodesManager {
|
||||
for(let k in allUsedNodes) {
|
||||
var item;
|
||||
if(allUsedNodes[k].properties.cnr_id) {
|
||||
item = this.custom_nodes[allUsedNodes[k].properties.cnr_id];
|
||||
const foundPackage = findPackageByCnrId(allUsedNodes[k].properties.cnr_id, this.custom_nodes, false);
|
||||
if (foundPackage) {
|
||||
item = foundPackage.pack;
|
||||
}
|
||||
}
|
||||
else if(allUsedNodes[k].properties.aux_id) {
|
||||
item = aux_id_to_pack[allUsedNodes[k].properties.aux_id];
|
||||
@ -1965,6 +1902,48 @@ export class CustomNodesManager {
|
||||
return hashMap;
|
||||
}
|
||||
|
||||
async getUsedInAnyWorkflow() {
|
||||
this.showStatus(`Loading workflow usage analysis ...`);
|
||||
|
||||
const result = await analyzeWorkflowUsage(this.custom_nodes);
|
||||
|
||||
if (!result.success) {
|
||||
this.showError(`Failed to get workflow data: ${result.error}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
const hashMap = {};
|
||||
|
||||
// Convert usage map keys to hash map
|
||||
result.usageMap.forEach((count, packageKey) => {
|
||||
const pack = this.custom_nodes[packageKey];
|
||||
if (pack && pack.hash) {
|
||||
hashMap[pack.hash] = true;
|
||||
}
|
||||
});
|
||||
|
||||
return hashMap;
|
||||
}
|
||||
|
||||
async getNotUsedInAnyWorkflow() {
|
||||
this.showStatus(`Loading workflow usage analysis ...`);
|
||||
|
||||
// Get the used packages first using common utility
|
||||
const usedHashMap = await this.getUsedInAnyWorkflow();
|
||||
const notUsedHashMap = {};
|
||||
|
||||
// Find all installed packages that are NOT in the used list
|
||||
for(let k in this.custom_nodes) {
|
||||
let nodepack = this.custom_nodes[k];
|
||||
// Only consider installed packages
|
||||
if (nodepack.state !== "not-installed" && !usedHashMap[nodepack.hash]) {
|
||||
notUsedHashMap[nodepack.hash] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return notUsedHashMap;
|
||||
}
|
||||
|
||||
async loadData(show_mode = ShowMode.NORMAL) {
|
||||
const isElectron = 'electronAPI' in window;
|
||||
|
||||
@ -2034,6 +2013,10 @@ export class CustomNodesManager {
|
||||
hashMap = await this.getFavorites();
|
||||
} else if(this.show_mode == ShowMode.IN_WORKFLOW) {
|
||||
hashMap = await this.getNodepackInWorkflow();
|
||||
} else if(this.show_mode == ShowMode.USED_IN_ANY_WORKFLOW) {
|
||||
hashMap = await this.getUsedInAnyWorkflow();
|
||||
} else if(this.show_mode == ShowMode.NOT_USED_IN_ANY_WORKFLOW) {
|
||||
hashMap = await this.getNotUsedInAnyWorkflow();
|
||||
}
|
||||
filterItem.hashMap = hashMap;
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { $el } from "../../scripts/ui.js";
|
||||
import {
|
||||
manager_instance, rebootAPI,
|
||||
fetchData, md5, icons, show_message, customAlert, infoToast, showTerminal,
|
||||
storeColumnWidth, restoreColumnWidth, loadCss, handle403Response
|
||||
storeColumnWidth, restoreColumnWidth, loadCss, formatSize, sizeToBytes
|
||||
} from "./common.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
@ -359,7 +359,7 @@ export class ModelManager {
|
||||
width: 100,
|
||||
formatter: (size) => {
|
||||
if (typeof size === "number") {
|
||||
return this.formatSize(size);
|
||||
return formatSize(size);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
@ -582,7 +582,7 @@ export class ModelManager {
|
||||
models.forEach((item, i) => {
|
||||
const { type, base, name, reference, installed } = item;
|
||||
item.originalData = JSON.parse(JSON.stringify(item));
|
||||
item.size = this.sizeToBytes(item.size);
|
||||
item.size = sizeToBytes(item.size);
|
||||
item.hash = md5(name + reference);
|
||||
item.id = i + 1;
|
||||
|
||||
@ -659,7 +659,6 @@ export class ModelManager {
|
||||
const { models } = res.data;
|
||||
|
||||
this.modelList = this.getModelList(models);
|
||||
// console.log("models", this.modelList);
|
||||
|
||||
this.updateFilter();
|
||||
|
||||
@ -671,56 +670,6 @@ export class ModelManager {
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
formatSize(v) {
|
||||
const base = 1000;
|
||||
const units = ['', 'K', 'M', 'G', 'T', 'P'];
|
||||
const space = '';
|
||||
const postfix = 'B';
|
||||
if (v <= 0) {
|
||||
return `0${space}${postfix}`;
|
||||
}
|
||||
for (let i = 0, l = units.length; i < l; i++) {
|
||||
const min = Math.pow(base, i);
|
||||
const max = Math.pow(base, i + 1);
|
||||
if (v > min && v <= max) {
|
||||
const unit = units[i];
|
||||
if (unit) {
|
||||
const n = v / min;
|
||||
const nl = n.toString().split('.')[0].length;
|
||||
const fl = Math.max(3 - nl, 1);
|
||||
v = n.toFixed(fl);
|
||||
}
|
||||
v = v + space + unit + postfix;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// for size sort
|
||||
sizeToBytes(v) {
|
||||
if (typeof v === "number") {
|
||||
return v;
|
||||
}
|
||||
if (typeof v === "string") {
|
||||
const n = parseFloat(v);
|
||||
const unit = v.replace(/[0-9.B]+/g, "").trim().toUpperCase();
|
||||
if (unit === "K") {
|
||||
return n * 1000;
|
||||
}
|
||||
if (unit === "M") {
|
||||
return n * 1000 * 1000;
|
||||
}
|
||||
if (unit === "G") {
|
||||
return n * 1000 * 1000 * 1000;
|
||||
}
|
||||
if (unit === "T") {
|
||||
return n * 1000 * 1000 * 1000 * 1000;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
showSelection(msg) {
|
||||
this.element.querySelector(".cmm-manager-selection").innerHTML = msg;
|
||||
}
|
||||
|
||||
699
js/node-usage-analyzer.css
Normal file
699
js/node-usage-analyzer.css
Normal file
@ -0,0 +1,699 @@
|
||||
.nu-manager {
|
||||
--grid-font: -apple-system, BlinkMacSystemFont, "Segue UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
z-index: 1099;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
color: var(--fg-color);
|
||||
font-family: arial, sans-serif;
|
||||
text-underline-offset: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.nu-manager .nu-flex-auto {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.nu-manager button {
|
||||
font-size: 16px;
|
||||
color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;
|
||||
margin: 0;
|
||||
padding: 4px 8px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.nu-manager button:disabled,
|
||||
.nu-manager input:disabled,
|
||||
.nu-manager select:disabled {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.nu-manager button:disabled {
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.nu-manager .nu-manager-restart {
|
||||
display: none;
|
||||
background-color: #500000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .nu-manager-stop {
|
||||
display: none;
|
||||
background-color: #500000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .nu-manager-back {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
margin-right: 5px;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.cn-icon {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.cn-icon svg {
|
||||
display: block;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nu-manager-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.nu-manager-header label {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nu-manager-filter {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.nu-manager-keywords {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 5px 0 26px;
|
||||
background-size: 16px;
|
||||
background-position: 5px center;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.nu-manager-status {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.nu-manager-grid {
|
||||
flex: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nu-manager-selection {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nu-manager-message {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nu-manager-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nu-manager-grid .tg-turbogrid {
|
||||
font-family: var(--grid-font);
|
||||
font-size: 15px;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.nu-manager-grid .tg-turbogrid .tg-highlight::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: #80bdff11;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nu-manager-grid .nu-pack-name a {
|
||||
color: skyblue;
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.nu-manager-grid .cn-pack-desc a {
|
||||
color: #5555FF;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nu-manager-grid .tg-cell a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nu-manager-grid .cn-pack-version {
|
||||
line-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.nu-manager-grid .cn-pack-nodes {
|
||||
line-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nu-manager-grid .cn-pack-nodes:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nu-manager-grid .cn-pack-conflicts {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.cn-popover {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
padding: 20px;
|
||||
color: #1e1e1e;
|
||||
filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cn-flyover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
animation-duration: 0.2s;
|
||||
animation-fill-mode: both;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cn-flyover::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
content: "";
|
||||
z-index: 10;
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
left: -10px;
|
||||
background-image: linear-gradient(to left, rgb(0 0 0 / 20%), rgb(0 0 0 / 0%));
|
||||
}
|
||||
|
||||
.cn-flyover-header {
|
||||
height: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.cn-flyover-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cn-flyover-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cn-flyover-close svg {
|
||||
display: block;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.cn-flyover-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
gap: 10px;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.cn-flyover-body {
|
||||
height: calc(100% - 45px);
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
}
|
||||
|
||||
@keyframes cn-slide-in-right {
|
||||
from {
|
||||
visibility: visible;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.cn-slide-in-right {
|
||||
animation-name: cn-slide-in-right;
|
||||
}
|
||||
|
||||
@keyframes cn-slide-out-right {
|
||||
from {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
visibility: hidden;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.cn-slide-out-right {
|
||||
animation-name: cn-slide-out-right;
|
||||
}
|
||||
|
||||
.cn-nodes-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cn-nodes-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cn-nodes-row:nth-child(odd) {
|
||||
background-color: rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
.cn-nodes-row:hover {
|
||||
background-color: rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.cn-nodes-sn {
|
||||
text-align: right;
|
||||
min-width: 35px;
|
||||
color: var(--drag-text);
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
padding: 8px 5px;
|
||||
}
|
||||
|
||||
.cn-nodes-name {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
padding: 8px 5px;
|
||||
}
|
||||
|
||||
.cn-nodes-name::after {
|
||||
content: attr(action);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
transform: translate(5px, -50%);
|
||||
font-size: 12px;
|
||||
color: var(--drag-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 3px 8px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cn-nodes-name.action::after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cn-nodes-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cn-nodes-conflict .cn-nodes-name,
|
||||
.cn-nodes-conflict .cn-icon {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.cn-conflicts-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.cn-conflicts-list b {
|
||||
font-weight: normal;
|
||||
color: var(--descrip-text);
|
||||
}
|
||||
|
||||
.cn-nodes-pack {
|
||||
cursor: pointer;
|
||||
color: skyblue;
|
||||
}
|
||||
|
||||
.cn-nodes-pack:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cn-pack-badge {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 3px 8px;
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.cn-preview {
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
min-height: 120px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
padding: 12px;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.cn-preview-header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--comfy-input-bg);
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.cn-preview-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: grey;
|
||||
position: relative;
|
||||
filter: drop-shadow(1px 2px 3px rgb(0 0 0 / 30%));
|
||||
}
|
||||
|
||||
.cn-preview-dot.cn-preview-optional::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 50%;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.cn-preview-dot.cn-preview-grid {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.cn-preview-dot.cn-preview-grid::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-left: 1px solid var(--comfy-input-bg);
|
||||
border-right: 1px solid var(--comfy-input-bg);
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
left: 2px;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cn-preview-dot.cn-preview-grid::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-top: 1px solid var(--comfy-input-bg);
|
||||
border-bottom: 1px solid var(--comfy-input-bg);
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
left: 0;
|
||||
top: 2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cn-preview-name {
|
||||
flex: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cn-preview-io {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
.cn-preview-column > div {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cn-preview-input {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.cn-preview-output {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cn-preview-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
|
||||
.cn-preview-switch {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--bg-color);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
text-wrap: nowrap;
|
||||
padding: 2px 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cn-preview-switch::before,
|
||||
.cn-preview-switch::after {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
color: var(--fg-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cn-preview-switch::before {
|
||||
content: "◀";
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.cn-preview-switch::after {
|
||||
content: "▶";
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.cn-preview-value {
|
||||
color: var(--descrip-text);
|
||||
}
|
||||
|
||||
.cn-preview-string {
|
||||
min-height: 30px;
|
||||
max-height: 300px;
|
||||
background: var(--bg-color);
|
||||
color: var(--descrip-text);
|
||||
border-radius: 3px;
|
||||
padding: 3px 5px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.cn-preview-description {
|
||||
margin: 0px 10px 10px 10px;
|
||||
padding: 6px;
|
||||
background: var(--border-color);
|
||||
color: var(--descrip-text);
|
||||
border-radius: 5px;
|
||||
font-style: italic;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cn-tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.cn-tag-list > div {
|
||||
background-color: var(--border-color);
|
||||
border-radius: 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.cn-install-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 3px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cn-selected-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-enable {
|
||||
background-color: #333399;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-disable {
|
||||
background-color: #442277;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-update {
|
||||
background-color: #1155AA;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-try-update {
|
||||
background-color: Gray;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-try-fix {
|
||||
background-color: #6495ED;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-import-failed {
|
||||
background-color: #AA1111;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-install {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-try-install {
|
||||
background-color: Gray;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-uninstall {
|
||||
background-color: #993333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-reinstall {
|
||||
background-color: #993333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nu-manager .cn-btn-switch {
|
||||
background-color: #448833;
|
||||
color: white;
|
||||
|
||||
}
|
||||
|
||||
@keyframes nu-btn-loading-bg {
|
||||
0% {
|
||||
left: 0;
|
||||
}
|
||||
100% {
|
||||
left: -105px;
|
||||
}
|
||||
}
|
||||
|
||||
.nu-manager button.nu-btn-loading {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-color: rgb(0 119 207 / 80%);
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.nu-manager button.nu-btn-loading::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
width: 500px;
|
||||
height: 100%;
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgb(0 119 207 / 30%),
|
||||
rgb(0 119 207 / 30%) 10px,
|
||||
transparent 10px,
|
||||
transparent 15px
|
||||
);
|
||||
animation: nu-btn-loading-bg 2s linear infinite;
|
||||
}
|
||||
|
||||
.nu-manager-light .nu-pack-name a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.nu-manager-light .cm-warn-note {
|
||||
background-color: #ccc !important;
|
||||
}
|
||||
|
||||
.nu-manager-light .cn-btn-install {
|
||||
background-color: #333;
|
||||
}
|
||||
742
js/node-usage-analyzer.js
Normal file
742
js/node-usage-analyzer.js
Normal file
@ -0,0 +1,742 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { $el } from "../../scripts/ui.js";
|
||||
import {
|
||||
manager_instance,
|
||||
fetchData, md5, show_message, customAlert, infoToast, showTerminal,
|
||||
storeColumnWidth, restoreColumnWidth, loadCss, uninstallNodes,
|
||||
analyzeWorkflowUsage, sizeToBytes, createFlyover, createUIStateManager
|
||||
} from "./common.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
// https://cenfun.github.io/turbogrid/api.html
|
||||
import TG from "./turbogrid.esm.js";
|
||||
|
||||
loadCss("./node-usage-analyzer.css");
|
||||
|
||||
const gridId = "model";
|
||||
|
||||
const pageHtml = `
|
||||
<div class="nu-manager-header">
|
||||
<div class="nu-manager-status"></div>
|
||||
<input type="text" class="nu-manager-keywords" placeholder="Filter keywords..." />
|
||||
<div class="nu-flex-auto"></div>
|
||||
</div>
|
||||
<div class="nu-manager-grid"></div>
|
||||
<div class="nu-manager-selection"></div>
|
||||
<div class="nu-manager-message"></div>
|
||||
<div class="nu-manager-footer">
|
||||
<button class="nu-manager-back">
|
||||
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Back
|
||||
</button>
|
||||
<button class="nu-manager-refresh">Refresh</button>
|
||||
<button class="nu-manager-stop">Stop</button>
|
||||
<div class="nu-flex-auto"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export class NodeUsageAnalyzer {
|
||||
static instance = null;
|
||||
|
||||
static SortMode = {
|
||||
BY_PACKAGE: 'by_package'
|
||||
};
|
||||
|
||||
constructor(app, manager_dialog) {
|
||||
this.app = app;
|
||||
this.manager_dialog = manager_dialog;
|
||||
this.id = "nu-manager";
|
||||
|
||||
this.filter = '';
|
||||
this.type = '';
|
||||
this.base = '';
|
||||
this.keywords = '';
|
||||
|
||||
this.init();
|
||||
|
||||
// Initialize shared UI state manager
|
||||
this.ui = createUIStateManager(this.element, {
|
||||
selection: ".nu-manager-selection",
|
||||
message: ".nu-manager-message",
|
||||
status: ".nu-manager-status",
|
||||
refresh: ".nu-manager-refresh",
|
||||
stop: ".nu-manager-stop"
|
||||
});
|
||||
|
||||
api.addEventListener("cm-queue-status", this.onQueueStatus);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.element = $el("div", {
|
||||
parent: document.body,
|
||||
className: "comfy-modal nu-manager"
|
||||
});
|
||||
this.element.innerHTML = pageHtml;
|
||||
this.bindEvents();
|
||||
this.initGrid();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
const eventsMap = {
|
||||
".nu-manager-selection": {
|
||||
click: (e) => {
|
||||
const target = e.target;
|
||||
const mode = target.getAttribute("mode");
|
||||
if (mode === "install") {
|
||||
this.installModels(this.selectedModels, target);
|
||||
} else if (mode === "uninstall") {
|
||||
this.uninstallModels(this.selectedModels, target);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
".nu-manager-refresh": {
|
||||
click: () => {
|
||||
app.refreshComboInNodes();
|
||||
}
|
||||
},
|
||||
|
||||
".nu-manager-stop": {
|
||||
click: () => {
|
||||
api.fetchApi('/manager/queue/reset');
|
||||
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
|
||||
}
|
||||
},
|
||||
|
||||
".nu-manager-back": {
|
||||
click: (e) => {
|
||||
this.close()
|
||||
manager_instance.show();
|
||||
}
|
||||
}
|
||||
};
|
||||
Object.keys(eventsMap).forEach(selector => {
|
||||
const target = this.element.querySelector(selector);
|
||||
if (target) {
|
||||
const events = eventsMap[selector];
|
||||
if (events) {
|
||||
Object.keys(events).forEach(type => {
|
||||
target.addEventListener(type, events[type]);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
initGrid() {
|
||||
const container = this.element.querySelector(".nu-manager-grid");
|
||||
const grid = new TG.Grid(container);
|
||||
this.grid = grid;
|
||||
|
||||
this.flyover = createFlyover(container, { context: this });
|
||||
|
||||
grid.bind('onUpdated', (e, d) => {
|
||||
this.ui.showStatus(`${grid.viewRows.length.toLocaleString()} installed packages`);
|
||||
|
||||
});
|
||||
|
||||
grid.bind('onSelectChanged', (e, changes) => {
|
||||
this.renderSelected();
|
||||
});
|
||||
|
||||
grid.bind("onColumnWidthChanged", (e, columnItem) => {
|
||||
storeColumnWidth(gridId, columnItem)
|
||||
});
|
||||
|
||||
grid.bind('onClick', (e, d) => {
|
||||
const { rowItem } = d;
|
||||
const target = d.e.target;
|
||||
const mode = target.getAttribute("mode");
|
||||
|
||||
if (mode === "install") {
|
||||
this.installModels([rowItem], target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === "uninstall") {
|
||||
this.uninstallModels([rowItem], target);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle click on usage count
|
||||
if (d.columnItem.id === "used_in_count" && rowItem.used_in_count > 0) {
|
||||
this.showUsageDetails(rowItem);
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
grid.setOption({
|
||||
theme: 'dark',
|
||||
|
||||
selectVisible: true,
|
||||
selectMultiple: true,
|
||||
selectAllVisible: true,
|
||||
|
||||
textSelectable: true,
|
||||
scrollbarRound: true,
|
||||
|
||||
frozenColumn: 1,
|
||||
rowNotFound: "No Results",
|
||||
|
||||
rowHeight: 40,
|
||||
bindWindowResize: true,
|
||||
bindContainerResize: true,
|
||||
|
||||
cellResizeObserver: (rowItem, columnItem) => {
|
||||
const autoHeightColumns = ['name', 'description'];
|
||||
return autoHeightColumns.includes(columnItem.id)
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
renderGrid() {
|
||||
|
||||
// update theme
|
||||
const colorPalette = this.app.ui.settings.settingsValues['Comfy.ColorPalette'];
|
||||
Array.from(this.element.classList).forEach(cn => {
|
||||
if (cn.startsWith("nu-manager-")) {
|
||||
this.element.classList.remove(cn);
|
||||
}
|
||||
});
|
||||
this.element.classList.add(`nu-manager-${colorPalette}`);
|
||||
|
||||
const options = {
|
||||
theme: colorPalette === "light" ? "" : "dark"
|
||||
};
|
||||
|
||||
const rows = this.modelList || [];
|
||||
|
||||
const columns = [{
|
||||
id: 'title',
|
||||
name: 'Title',
|
||||
width: 200,
|
||||
minWidth: 100,
|
||||
maxWidth: 500,
|
||||
classMap: 'nu-pack-name',
|
||||
formatter: function (name, rowItem, columnItem, cellNode) {
|
||||
return `<a href=${rowItem.reference} target="_blank"><b>${name}</b></a>`;
|
||||
}
|
||||
}, {
|
||||
id: 'used_in_count',
|
||||
name: 'Used in',
|
||||
width: 100,
|
||||
formatter: function (usedCount, rowItem, columnItem) {
|
||||
if (!usedCount || usedCount === 0) {
|
||||
return '0';
|
||||
}
|
||||
const plural = usedCount > 1 ? 's' : '';
|
||||
return `<div class="cn-pack-nodes" style="cursor: pointer;">${usedCount} workflow${plural}</div>`;
|
||||
}
|
||||
}, {
|
||||
id: 'action',
|
||||
name: 'Action',
|
||||
width: 160,
|
||||
minWidth: 140,
|
||||
maxWidth: 200,
|
||||
sortable: false,
|
||||
align: 'center',
|
||||
formatter: function (action, rowItem, columnItem) {
|
||||
// Only show uninstall button for installed packages
|
||||
if (rowItem.originalData && rowItem.originalData.state && rowItem.originalData.state !== "not-installed") {
|
||||
return `<div class="cn-install-buttons"><button class="nu-btn-uninstall" mode="uninstall">Uninstall</button></div>`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}];
|
||||
|
||||
restoreColumnWidth(gridId, columns);
|
||||
|
||||
this.grid.setData({
|
||||
options,
|
||||
rows,
|
||||
columns
|
||||
});
|
||||
|
||||
this.grid.render();
|
||||
|
||||
}
|
||||
|
||||
updateGrid() {
|
||||
if (this.grid) {
|
||||
this.grid.update();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
showUsageDetails(rowItem) {
|
||||
const workflowList = rowItem.workflowDetails;
|
||||
if (!workflowList || workflowList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let titleHtml = `<div class="cn-nodes-pack">${rowItem.title}</div>`;
|
||||
|
||||
const list = [];
|
||||
list.push(`<div class="cn-nodes-list">`);
|
||||
|
||||
workflowList.forEach((workflow, i) => {
|
||||
list.push(`<div class="cn-nodes-row">`);
|
||||
list.push(`<div class="cn-nodes-sn">${i + 1}</div>`);
|
||||
list.push(`<div class="cn-nodes-name">${workflow.filename}</div>`);
|
||||
list.push(`<div class="cn-nodes-details">${workflow.nodeCount} node${workflow.nodeCount > 1 ? 's' : ''}</div>`);
|
||||
list.push(`</div>`);
|
||||
});
|
||||
|
||||
list.push("</div>");
|
||||
const bodyHtml = list.join("");
|
||||
|
||||
this.flyover.show(titleHtml, bodyHtml);
|
||||
}
|
||||
|
||||
renderSelected() {
|
||||
const selectedList = this.grid.getSelectedRows();
|
||||
if (!selectedList.length) {
|
||||
this.ui.showSelection("");
|
||||
return;
|
||||
}
|
||||
|
||||
const installedSelected = selectedList.filter(item =>
|
||||
item.originalData && item.originalData.state && item.originalData.state !== "not-installed"
|
||||
);
|
||||
|
||||
if (installedSelected.length === 0) {
|
||||
this.ui.showSelection(`<span>Selected <b>${selectedList.length}</b> packages (none can be uninstalled)</span>`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedModels = installedSelected;
|
||||
|
||||
this.ui.showSelection(`
|
||||
<div class="nu-selected-buttons">
|
||||
<span>Selected <b>${installedSelected.length}</b> installed packages</span>
|
||||
<button class="nu-btn-uninstall" mode="uninstall">Uninstall Selected</button>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
async installModels(list, btn) {
|
||||
let stats = await api.fetchApi('/manager/queue/status');
|
||||
|
||||
stats = await stats.json();
|
||||
if (stats.is_processing) {
|
||||
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
|
||||
return;
|
||||
}
|
||||
|
||||
btn.classList.add("nu-btn-loading");
|
||||
this.ui.showError("");
|
||||
|
||||
let needRefresh = false;
|
||||
let errorMsg = "";
|
||||
|
||||
await api.fetchApi('/manager/queue/reset');
|
||||
|
||||
let target_items = [];
|
||||
|
||||
for (const item of list) {
|
||||
this.grid.scrollRowIntoView(item);
|
||||
target_items.push(item);
|
||||
|
||||
|
||||
this.ui.showStatus(`Install ${item.name} ...`);
|
||||
|
||||
const data = item.originalData;
|
||||
data.ui_id = item.hash;
|
||||
|
||||
const res = await api.fetchApi(`/manager/queue/install_model`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (res.status != 200) {
|
||||
errorMsg = `'${item.name}': `;
|
||||
|
||||
if (res.status == 403) {
|
||||
errorMsg += `This action is not allowed with this security level configuration.\n`;
|
||||
} else {
|
||||
errorMsg += await res.text() + '\n';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.install_context = { btn: btn, targets: target_items };
|
||||
|
||||
if (errorMsg) {
|
||||
this.ui.showError(errorMsg);
|
||||
show_message("[Installation Errors]\n" + errorMsg);
|
||||
|
||||
// reset
|
||||
for (let k in target_items) {
|
||||
const item = target_items[k];
|
||||
this.grid.updateCell(item, "installed");
|
||||
}
|
||||
}
|
||||
else {
|
||||
await api.fetchApi('/manager/queue/start');
|
||||
this.ui.showStop();
|
||||
showTerminal();
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallModels(list, btn) {
|
||||
btn.classList.add("nu-btn-loading");
|
||||
this.ui.showError("");
|
||||
|
||||
const result = await uninstallNodes(list, {
|
||||
title: list.length === 1 ? list[0].title || list[0].name : `${list.length} custom nodes`,
|
||||
channel: 'default',
|
||||
mode: 'default',
|
||||
onProgress: (msg) => {
|
||||
this.showStatus(msg);
|
||||
},
|
||||
onError: (errorMsg) => {
|
||||
this.showError(errorMsg);
|
||||
},
|
||||
onSuccess: (targets) => {
|
||||
this.showStatus(`Uninstalled ${targets.length} custom node(s) successfully`);
|
||||
this.showMessage(`To apply the uninstalled custom nodes, please restart ComfyUI and refresh browser.`, "red");
|
||||
// Update the grid to reflect changes
|
||||
for (let item of targets) {
|
||||
if (item.originalData) {
|
||||
item.originalData.state = "not-installed";
|
||||
}
|
||||
this.grid.updateRow(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.showStop();
|
||||
}
|
||||
|
||||
btn.classList.remove("nu-btn-loading");
|
||||
}
|
||||
|
||||
async onQueueStatus(event) {
|
||||
let self = NodeUsageAnalyzer.instance;
|
||||
|
||||
if (event.detail.status == 'in_progress' && (event.detail.ui_target == 'model_manager' || event.detail.ui_target == 'nodepack_manager')) {
|
||||
const hash = event.detail.target;
|
||||
|
||||
const item = self.grid.getRowItemBy("hash", hash);
|
||||
|
||||
if (item) {
|
||||
item.refresh = true;
|
||||
self.grid.setRowSelected(item, false);
|
||||
item.selectable = false;
|
||||
self.grid.updateRow(item);
|
||||
}
|
||||
}
|
||||
else if (event.detail.status == 'done') {
|
||||
self.hideStop();
|
||||
self.onQueueCompleted(event.detail);
|
||||
}
|
||||
}
|
||||
|
||||
async onQueueCompleted(info) {
|
||||
let result = info.model_result || info.nodepack_result;
|
||||
|
||||
if (!result || result.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let self = NodeUsageAnalyzer.instance;
|
||||
|
||||
if (!self.install_context) {
|
||||
return;
|
||||
}
|
||||
|
||||
let btn = self.install_context.btn;
|
||||
|
||||
self.hideLoading();
|
||||
btn.classList.remove("nu-btn-loading");
|
||||
|
||||
let errorMsg = "";
|
||||
|
||||
for (let hash in result) {
|
||||
let v = result[hash];
|
||||
|
||||
if (v != 'success' && v != 'skip')
|
||||
errorMsg += v + '\n';
|
||||
}
|
||||
|
||||
for (let k in self.install_context.targets) {
|
||||
let item = self.install_context.targets[k];
|
||||
if (info.model_result) {
|
||||
self.grid.updateCell(item, "installed");
|
||||
} else if (info.nodepack_result) {
|
||||
// Handle uninstall completion
|
||||
if (item.originalData) {
|
||||
item.originalData.state = "not-installed";
|
||||
}
|
||||
self.grid.updateRow(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
self.showError(errorMsg);
|
||||
show_message("Operation Error:\n" + errorMsg);
|
||||
} else {
|
||||
if (info.model_result) {
|
||||
self.showStatus(`Install ${Object.keys(result).length} models successfully`);
|
||||
self.showRefresh();
|
||||
self.showMessage(`To apply the installed model, please click the 'Refresh' button.`, "red");
|
||||
} else if (info.nodepack_result) {
|
||||
self.showStatus(`Uninstall ${Object.keys(result).length} custom node(s) successfully`);
|
||||
self.showMessage(`To apply the uninstalled custom nodes, please restart ComfyUI and refresh browser.`, "red");
|
||||
}
|
||||
}
|
||||
|
||||
infoToast('Tasks done', `[ComfyUI-Manager] All tasks in the queue have been completed.\n${info.done_count}/${info.total_count}`);
|
||||
self.install_context = undefined;
|
||||
}
|
||||
|
||||
getModelList(models) {
|
||||
const typeMap = new Map();
|
||||
const baseMap = new Map();
|
||||
|
||||
models.forEach((item, i) => {
|
||||
const { type, base, name, reference, installed } = item;
|
||||
// CRITICAL FIX: Do NOT overwrite originalData - it contains the needed state field!
|
||||
item.size = sizeToBytes(item.size);
|
||||
item.hash = md5(name + reference);
|
||||
|
||||
if (installed === "True") {
|
||||
item.selectable = false;
|
||||
}
|
||||
|
||||
typeMap.set(type, type);
|
||||
baseMap.set(base, base);
|
||||
|
||||
});
|
||||
|
||||
const typeList = [];
|
||||
typeMap.forEach(type => {
|
||||
typeList.push({
|
||||
label: type,
|
||||
value: type
|
||||
});
|
||||
});
|
||||
typeList.sort((a, b) => {
|
||||
const au = a.label.toUpperCase();
|
||||
const bu = b.label.toUpperCase();
|
||||
if (au !== bu) {
|
||||
return au > bu ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
this.typeList = [{
|
||||
label: "All",
|
||||
value: ""
|
||||
}].concat(typeList);
|
||||
|
||||
|
||||
const baseList = [];
|
||||
baseMap.forEach(base => {
|
||||
baseList.push({
|
||||
label: base,
|
||||
value: base
|
||||
});
|
||||
});
|
||||
baseList.sort((a, b) => {
|
||||
const au = a.label.toUpperCase();
|
||||
const bu = b.label.toUpperCase();
|
||||
if (au !== bu) {
|
||||
return au > bu ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
this.baseList = [{
|
||||
label: "All",
|
||||
value: ""
|
||||
}].concat(baseList);
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
async loadData() {
|
||||
|
||||
this.showLoading();
|
||||
this.showStatus(`Analyzing node usage ...`);
|
||||
|
||||
const mode = manager_instance.datasrc_combo.value;
|
||||
|
||||
const nodeListRes = await fetchData(`/customnode/getlist?mode=${mode}&skip_update=true`);
|
||||
if (nodeListRes.error) {
|
||||
this.showError("Failed to get custom node list.");
|
||||
this.hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
const { channel, node_packs } = nodeListRes.data;
|
||||
delete node_packs['comfyui-manager'];
|
||||
this.installed_custom_node_packs = node_packs;
|
||||
|
||||
// Use the consolidated workflow analysis utility
|
||||
const result = await analyzeWorkflowUsage(node_packs);
|
||||
|
||||
if (!result.success) {
|
||||
if (result.error.toString().includes('204')) {
|
||||
this.showMessage("No workflows were found for analysis.");
|
||||
} else {
|
||||
this.showError(result.error);
|
||||
this.hideLoading();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform node_packs into models format - ONLY INSTALLED PACKAGES
|
||||
const models = [];
|
||||
|
||||
Object.keys(node_packs).forEach((packKey, index) => {
|
||||
const pack = node_packs[packKey];
|
||||
|
||||
// Only include installed packages (filter out "not-installed" packages)
|
||||
if (pack.state === "not-installed") {
|
||||
return; // Skip non-installed packages
|
||||
}
|
||||
|
||||
const usedCount = result.usageMap?.get(packKey) || 0;
|
||||
const workflowDetails = result.workflowDetailsMap?.get(packKey) || [];
|
||||
|
||||
models.push({
|
||||
title: pack.title || packKey,
|
||||
reference: pack.reference || pack.files?.[0] || '#',
|
||||
used_in_count: usedCount,
|
||||
workflowDetails: workflowDetails,
|
||||
name: packKey,
|
||||
originalData: pack
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by usage count (descending) then by title
|
||||
models.sort((a, b) => {
|
||||
if (b.used_in_count !== a.used_in_count) {
|
||||
return b.used_in_count - a.used_in_count;
|
||||
}
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
|
||||
this.modelList = this.getModelList(models);
|
||||
|
||||
this.renderGrid();
|
||||
|
||||
this.hideLoading();
|
||||
|
||||
}
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
showSelection(msg) {
|
||||
this.element.querySelector(".nu-manager-selection").innerHTML = msg;
|
||||
}
|
||||
|
||||
showError(err) {
|
||||
this.showMessage(err, "red");
|
||||
}
|
||||
|
||||
showMessage(msg, color) {
|
||||
if (color) {
|
||||
msg = `<font color="${color}">${msg}</font>`;
|
||||
}
|
||||
this.element.querySelector(".nu-manager-message").innerHTML = msg;
|
||||
}
|
||||
|
||||
showStatus(msg, color) {
|
||||
if (color) {
|
||||
msg = `<font color="${color}">${msg}</font>`;
|
||||
}
|
||||
this.element.querySelector(".nu-manager-status").innerHTML = msg;
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
// this.setDisabled(true);
|
||||
if (this.grid) {
|
||||
this.grid.showLoading();
|
||||
this.grid.showMask({
|
||||
opacity: 0.05
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
// this.setDisabled(false);
|
||||
if (this.grid) {
|
||||
this.grid.hideLoading();
|
||||
this.grid.hideMask();
|
||||
}
|
||||
}
|
||||
|
||||
setDisabled(disabled) {
|
||||
const $close = this.element.querySelector(".nu-manager-close");
|
||||
const $refresh = this.element.querySelector(".nu-manager-refresh");
|
||||
const $stop = this.element.querySelector(".nu-manager-stop");
|
||||
|
||||
const list = [
|
||||
".nu-manager-header input",
|
||||
".nu-manager-header select",
|
||||
".nu-manager-footer button",
|
||||
".nu-manager-selection button"
|
||||
].map(s => {
|
||||
return Array.from(this.element.querySelectorAll(s));
|
||||
})
|
||||
.flat()
|
||||
.filter(it => {
|
||||
return it !== $close && it !== $refresh && it !== $stop;
|
||||
});
|
||||
|
||||
list.forEach($elem => {
|
||||
if (disabled) {
|
||||
$elem.setAttribute("disabled", "disabled");
|
||||
} else {
|
||||
$elem.removeAttribute("disabled");
|
||||
}
|
||||
});
|
||||
|
||||
Array.from(this.element.querySelectorAll(".nu-btn-loading")).forEach($elem => {
|
||||
$elem.classList.remove("nu-btn-loading");
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
showRefresh() {
|
||||
this.element.querySelector(".nu-manager-refresh").style.display = "block";
|
||||
}
|
||||
|
||||
showStop() {
|
||||
this.element.querySelector(".nu-manager-stop").style.display = "block";
|
||||
}
|
||||
|
||||
hideStop() {
|
||||
this.element.querySelector(".nu-manager-stop").style.display = "none";
|
||||
}
|
||||
|
||||
setKeywords(keywords = "") {
|
||||
this.keywords = keywords;
|
||||
this.element.querySelector(".nu-manager-keywords").value = keywords;
|
||||
}
|
||||
|
||||
show(sortMode) {
|
||||
this.element.style.display = "flex";
|
||||
this.setKeywords("");
|
||||
this.showSelection("");
|
||||
this.showMessage("");
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.element.style.display = "none";
|
||||
}
|
||||
}
|
||||
@ -3826,6 +3826,7 @@
|
||||
"MaskClampedCrop",
|
||||
"MaskClampedCropSticky",
|
||||
"SaveConditioning",
|
||||
"SaveImagePlus",
|
||||
"SaveInpaintCropCache"
|
||||
],
|
||||
{
|
||||
@ -4735,16 +4736,28 @@
|
||||
"CloseHost",
|
||||
"CompileConfig",
|
||||
"CreateHost",
|
||||
"DDIMScheduler",
|
||||
"DDPMScheduler",
|
||||
"DEISMultistepScheduler",
|
||||
"DPMSolverMultistepScheduler",
|
||||
"DPMSolverSinglestepScheduler",
|
||||
"EncodePromptWithCompel",
|
||||
"EulerAncestralDiscreteScheduler",
|
||||
"EulerDiscreteScheduler",
|
||||
"FlowMatchScheduler",
|
||||
"FlowMatchSchedulerConfig",
|
||||
"FluxSampler",
|
||||
"GroupOffloadConfig",
|
||||
"HeunDiscreteScheduler",
|
||||
"KDPM2AncestralDiscreteScheduler",
|
||||
"KDPM2DiscreteScheduler",
|
||||
"LMSDiscreteScheduler",
|
||||
"LoraSelector",
|
||||
"ModelSelector",
|
||||
"MultiLoraJoiner",
|
||||
"OffloadConfig",
|
||||
"OffloadPipeline",
|
||||
"PNDMScheduler",
|
||||
"QuantizationConfig",
|
||||
"QuantoQuantizationConfig",
|
||||
"SDNQQuantizationConfig",
|
||||
@ -4752,12 +4765,12 @@
|
||||
"SDSamplerPrompt",
|
||||
"SDUpscaleSampler",
|
||||
"SVDSampler",
|
||||
"SchedulerConfig",
|
||||
"SchedulerSelector",
|
||||
"SingleConfig",
|
||||
"SleepHost",
|
||||
"TCDScheduler",
|
||||
"TorchAOQuantizationConfig",
|
||||
"TorchConfig",
|
||||
"UniPCMultistepScheduler",
|
||||
"UnsafeModelSelector",
|
||||
"WanSampler",
|
||||
"ZImageSampler"
|
||||
@ -7209,6 +7222,7 @@
|
||||
"GeminiNanoBanana2V2",
|
||||
"GeminiNode",
|
||||
"GenerateTracks",
|
||||
"GetICLoRAParameters",
|
||||
"GetImageSize",
|
||||
"GetVideoComponents",
|
||||
"GrokImageEditNode",
|
||||
@ -12459,6 +12473,7 @@
|
||||
"Fast Groups Bypasser [Eclipse]",
|
||||
"Fast Groups Muter [Eclipse]",
|
||||
"Fast Mode Switcher [Eclipse]",
|
||||
"Fast Mode Toggle [Eclipse]",
|
||||
"Fast Muter [Eclipse]",
|
||||
"Float Passer [Eclipse]",
|
||||
"Float [Eclipse]",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,129 @@
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"author": "dreamrec",
|
||||
"title": "ComfyUI-Pixal3D",
|
||||
"id": "comfyui-pixal3d",
|
||||
"reference": "https://github.com/dreamrec/ComfyUI-Pixal3D",
|
||||
"files": [
|
||||
"https://github.com/dreamrec/ComfyUI-Pixal3D"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI wrapper for [a/Pixal3D](https://github.com/TencentARC/Pixal3D) — Tencent's SIGGRAPH 2026 single-image to PBR-textured-3D pipeline. Windows + NVIDIA RTX 30/40/50 (≥ 16 GB VRAM). One image → textured PBR mesh in ~3-5 min. Built on top of [a/ComfyUI-Trellis2](https://github.com/visualbruno/ComfyUI-Trellis2). Note: Pixal3D itself is academic / non-commercial only and NOT for EU use — see NOTICE.md."
|
||||
},
|
||||
{
|
||||
"author": "PlagueKind",
|
||||
"title": "PlagueKind-Nodes",
|
||||
"reference": "https://github.com/PlagueKind/ComfyUI-PlagueKind-Nodes",
|
||||
"files": [
|
||||
"https://github.com/PlagueKind/ComfyUI-PlagueKind-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Custom ComfyUI nodes providing unified image and mask resizing with multiple scaling modes, aspect-ratio preservation, center crop alignment, and stable tensor-based mask transformations."
|
||||
},
|
||||
{
|
||||
"author": "ketle-man",
|
||||
"title": "Model and Prompt from Metadata",
|
||||
"id": "model-and-prompt-from-metadata",
|
||||
"reference": "https://github.com/ketle-man/model-and-prompt-from-metadata",
|
||||
"files": [
|
||||
"https://github.com/ketle-man/model-and-prompt-from-metadata"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Drop a ComfyUI PNG/WebP/JSON onto the node to instantly extract and apply checkpoint, VAE, and prompts from embedded metadata. Supports ComfyUI, SD WebUI, SD Forge neo, Fooocus, SDXLPromptStyler, and Lora Loader (LoraManager). Includes LoRA from Metadata and CLIP Text Encode edit+ nodes."
|
||||
},
|
||||
{
|
||||
"author": "Magos Digital Studio",
|
||||
"title": "ComfyUI-Magos-Nodes",
|
||||
"reference": "https://github.com/MagosDigitalStudio/ComfyUI-Magos-Nodes",
|
||||
"files": [
|
||||
"https://github.com/MagosDigitalStudio/ComfyUI-Magos-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "DWPose & NLF skeleton editor, retargeter, and renderer for ComfyUI. Extract body/hand/face keypoints (+ optional NLF 3D) from video, edit them frame-by-frame in a full-screen timeline + dope sheet + graph + 3D orbit editor with animated camera, retarget DWPose proportions per-cluster, and render clean pose images. Outputs standard ComfyUI types — drives WanAnimate, ControlNet, SCAIL, LTX-Video, UniAnimate, and any other pose-driven pipeline."
|
||||
},
|
||||
{
|
||||
"author": "Anonymzx",
|
||||
"title": "BangtrixToolkit",
|
||||
"reference": "https://github.com/Anonymzx/BangtrixToolkit",
|
||||
"files": [
|
||||
"https://github.com/Anonymzx/BangtrixToolkit"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Advanced ComfyUI toolkit with translation, AMD ROCm utilities, optimization, and workflow enhance"
|
||||
},
|
||||
{
|
||||
"author": "pmarmotte2",
|
||||
"title": "Comfyui-Pick_Any",
|
||||
"reference": "https://github.com/pmarmotte2/Comfyui_Pick_Any",
|
||||
"files": [
|
||||
"https://github.com/pmarmotte2/Comfyui_Pick_Any"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Advanced chooser nodes for ComfyUI that let workflows pause for image, audio, or text selection, with optional automatic picking and series helper nodes."
|
||||
},
|
||||
{
|
||||
"author": "WJLUOXIAO",
|
||||
"title": "XB_ToolBox",
|
||||
"reference": "https://github.com/WJLUOXIAO/XB_ToolBox",
|
||||
"files": [
|
||||
"https://github.com/WJLUOXIAO/XB_ToolBox"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A comprehensive ComfyUI extension suite encompassing both front-end interaction and low-level memory scheduling, designed to help beginners master workflows and simplify local deployment."
|
||||
},
|
||||
{
|
||||
"author": "PaoloC68",
|
||||
"title": "ComfyUI-PuLID-Flux-Chroma",
|
||||
"id": "comfyui-pulid-flux-chroma",
|
||||
"reference": "https://github.com/PaoloC68/ComfyUI-PuLID-Flux-Chroma",
|
||||
"files": [
|
||||
"https://github.com/PaoloC68/ComfyUI-PuLID-Flux-Chroma"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "The first PuLID (face identity preservation) implementation with full Chroma model support for ComfyUI. Automatically detects and adapts to both FLUX and Chroma models. Fork of PuLID-Flux-Enhanced with comprehensive Chroma compatibility fixes.",
|
||||
"stars": 0,
|
||||
"last_update": "2025-06-15 00:00:00",
|
||||
"preemptions": [
|
||||
"ApplyPulidFlux",
|
||||
"PulidFluxModelLoader",
|
||||
"PulidFluxInsightFaceLoader",
|
||||
"PulidFluxEvaClipLoader"
|
||||
],
|
||||
"nodename_pattern": "PulidFlux",
|
||||
"badges": [
|
||||
"Chroma",
|
||||
"Face Identity"
|
||||
],
|
||||
"dependencies": [
|
||||
"insightface",
|
||||
"onnxruntime"
|
||||
],
|
||||
"pip": [
|
||||
"insightface",
|
||||
"onnxruntime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"author": "flrngel",
|
||||
"title": "ComfyUI_rgbx_xrgb_Wrapper",
|
||||
"reference": "https://github.com/flrngel/ComfyUI_rgbx_xrgb_Wrapper",
|
||||
"files": [
|
||||
"https://github.com/flrngel/ComfyUI_rgbx_xrgb_Wrapper"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This is the rgb2x and x2rgb wrapper node for ComfyUI. The required models are automatically downloaded on the first run.\noriginal project : [a/https://github.com/zheng95z/rgbx](original project : https://github.com/zheng95z/rgbx)"
|
||||
},
|
||||
{
|
||||
"author": "nicehero",
|
||||
"title": "comfyui-conditioning-saver",
|
||||
"title": "ComfyUI Conditioning Saver",
|
||||
"id": "conditioningsaver",
|
||||
"reference": "https://github.com/nicehero/comfyui-conditioning-saver",
|
||||
"files": [
|
||||
"https://github.com/nicehero/comfyui-conditioning-saver"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Save and load CONDITIONING tensors to/from files, enabling reuse of encoded prompts and caching expensive CLIP encodings across workflows."
|
||||
"description": "Save and load CONDITIONING tensors to or from files, enabling reuse of encoded prompts and caching expensive CLIP encodings across workflows."
|
||||
},
|
||||
{
|
||||
"author": "randyhaylor",
|
||||
|
||||
@ -2956,6 +2956,14 @@
|
||||
"title_aux": "ComfyUI Qwen Prompt Expander"
|
||||
}
|
||||
],
|
||||
"https://github.com/Anonymzx/BangtrixToolkit": [
|
||||
[
|
||||
"BangtrixTranslateUniversal"
|
||||
],
|
||||
{
|
||||
"title_aux": "BangtrixToolkit"
|
||||
}
|
||||
],
|
||||
"https://github.com/Anzhc/Anima-Mod-Guidance-ComfyUI-Node": [
|
||||
[
|
||||
"AnimaModGuidance"
|
||||
@ -6779,7 +6787,8 @@
|
||||
"DenoLTXSequencer",
|
||||
"DenoMultiImageLoader",
|
||||
"DenoRTXVFXEasyUpscale",
|
||||
"DenoResolutionSetup"
|
||||
"DenoResolutionSetup",
|
||||
"DenoVideoCompare"
|
||||
],
|
||||
{
|
||||
"title_aux": "Deno Custom Nodes"
|
||||
@ -14863,6 +14872,19 @@
|
||||
"title_aux": "comfyui-vram-overlay"
|
||||
}
|
||||
],
|
||||
"https://github.com/MagosDigitalStudio/ComfyUI-Magos-Nodes": [
|
||||
[
|
||||
"DWPoseTEEditor",
|
||||
"DWPoseTEExtractor",
|
||||
"DWPoseTERenderer",
|
||||
"MagosPoseRetargeter",
|
||||
"WanAnimateSamplerPresets",
|
||||
"WanRatioAndFPS"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-Magos-Nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager": [
|
||||
[
|
||||
"MajoorSaveImage",
|
||||
@ -17478,6 +17500,24 @@
|
||||
"title_aux": "ComfyUI-Gallery"
|
||||
}
|
||||
],
|
||||
"https://github.com/PaoloC68/ComfyUI-PuLID-Flux-Chroma": [
|
||||
[
|
||||
"ApplyPulidFlux",
|
||||
"PulidFluxEvaClipLoader",
|
||||
"PulidFluxInsightFaceLoader",
|
||||
"PulidFluxModelLoader"
|
||||
],
|
||||
{
|
||||
"nodename_pattern": "PulidFlux",
|
||||
"preemptions": [
|
||||
"ApplyPulidFlux",
|
||||
"PulidFluxModelLoader",
|
||||
"PulidFluxInsightFaceLoader",
|
||||
"PulidFluxEvaClipLoader"
|
||||
],
|
||||
"title_aux": "ComfyUI-PuLID-Flux-Chroma"
|
||||
}
|
||||
],
|
||||
"https://github.com/Parameshvadivel/ComfyUI-SVGview": [
|
||||
[
|
||||
"SVGPreview"
|
||||
@ -17769,6 +17809,14 @@
|
||||
"title_aux": "Joy Caption Two - PixelaiLabs Edition"
|
||||
}
|
||||
],
|
||||
"https://github.com/PlagueKind/ComfyUI-PlagueKind-Nodes": [
|
||||
[
|
||||
"UnifiedResizeImageMask"
|
||||
],
|
||||
{
|
||||
"title_aux": "PlagueKind-Nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/PnthrLeo/comfyUI-PL-data-tools": [
|
||||
[
|
||||
"AreasGenerator",
|
||||
@ -18165,6 +18213,10 @@
|
||||
],
|
||||
"https://github.com/PozzettiAndrea/ComfyUI-TurntableGSViewer": [
|
||||
[
|
||||
"GaussianAnalysis",
|
||||
"GaussianExport",
|
||||
"GaussianMerge",
|
||||
"LoadPLY",
|
||||
"PreviewGaussians"
|
||||
],
|
||||
{
|
||||
@ -24083,6 +24135,35 @@
|
||||
"title_aux": "WAS Affine"
|
||||
}
|
||||
],
|
||||
"https://github.com/WJLUOXIAO/XB_ToolBox": [
|
||||
[
|
||||
"XB_BatchFolderLoader",
|
||||
"XB_CLIPNameBroadcaster",
|
||||
"XB_CheckpointBlockSwap",
|
||||
"XB_ChunkVisualization",
|
||||
"XB_Dashboard_Zen",
|
||||
"XB_DynamicBus",
|
||||
"XB_ImageParamsMaster",
|
||||
"XB_MasterParameter",
|
||||
"XB_SageAttentionAccelerator",
|
||||
"XB_SamplerChunkMaster",
|
||||
"XB_StoryboardSlicer",
|
||||
"XB_UNetBlockSwap",
|
||||
"XB_UNetNameBroadcaster",
|
||||
"XB_VRAM_Calculator",
|
||||
"XB_VideoParamsMaster",
|
||||
"XB_Video_Merger",
|
||||
"XB_WanFirstLastFrameToVideo",
|
||||
"XB_WanImageToVideo",
|
||||
"XB_Wan_ParamBus",
|
||||
"XB_Wan_RelayNode",
|
||||
"XTX_Data_Radar",
|
||||
"XTX_VRAM_Cleaner"
|
||||
],
|
||||
{
|
||||
"title_aux": "XB_ToolBox"
|
||||
}
|
||||
],
|
||||
"https://github.com/WUYUDING2583/ComfyUI-Save-Image-Callback": [
|
||||
[
|
||||
"Save Image With Callback"
|
||||
@ -30977,7 +31058,6 @@
|
||||
"IO_ImageSaveOverwrite",
|
||||
"IO_LoadAudioBatch",
|
||||
"IO_LoadImgBatch",
|
||||
"IO_LoadImgList",
|
||||
"IO_LoadShotBatch",
|
||||
"IO_LoadTextBatch",
|
||||
"IO_LoadVideoBatch",
|
||||
@ -31205,6 +31285,7 @@
|
||||
"mask_sam_detctor",
|
||||
"math_Remap_data",
|
||||
"math_calculate",
|
||||
"math_text_compare",
|
||||
"model_tool_assy",
|
||||
"pack_Pack",
|
||||
"pack_Unpack",
|
||||
@ -36177,6 +36258,16 @@
|
||||
"title_aux": "ComfyUI_show_seed"
|
||||
}
|
||||
],
|
||||
"https://github.com/dreamrec/ComfyUI-Pixal3D": [
|
||||
[
|
||||
"Pixal3DFreePipeline",
|
||||
"Pixal3DImageToMesh",
|
||||
"Pixal3DLoadPipeline"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-Pixal3D"
|
||||
}
|
||||
],
|
||||
"https://github.com/drmbt/comfyui-dreambait-nodes": [
|
||||
[
|
||||
"AudioInfoPlus",
|
||||
@ -38258,6 +38349,15 @@
|
||||
"title_aux": "ComfyUI-Flowty-TripoSR"
|
||||
}
|
||||
],
|
||||
"https://github.com/flrngel/ComfyUI_rgbx_xrgb_Wrapper": [
|
||||
[
|
||||
"rgb2x",
|
||||
"x2rgb"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI_rgbx_xrgb_Wrapper"
|
||||
}
|
||||
],
|
||||
"https://github.com/fluffydiveX/ComfyUI-hvBlockswap": [
|
||||
[
|
||||
"hvBlockSwap"
|
||||
@ -40444,6 +40544,7 @@
|
||||
"Double_Prompt_Encode",
|
||||
"FiveRandomLines",
|
||||
"Prompt_Extender",
|
||||
"Prompt_Library",
|
||||
"RandomLine",
|
||||
"Simple_Prompt_Library"
|
||||
],
|
||||
@ -44338,6 +44439,17 @@
|
||||
"title_aux": "ComfyUI VRM Pose Editor 3D"
|
||||
}
|
||||
],
|
||||
"https://github.com/ketle-man/model-and-prompt-from-metadata": [
|
||||
[
|
||||
"CLIPTextEncodeEditPlus",
|
||||
"ImageMetadataCheckpointLoader",
|
||||
"ImageMetadataLoRALoader",
|
||||
"ImageMetadataPromptLoader"
|
||||
],
|
||||
{
|
||||
"title_aux": "Model and Prompt from Metadata"
|
||||
}
|
||||
],
|
||||
"https://github.com/kevinmcmahondev/comfyui-kmcdev-image-filter-adjustments": [
|
||||
[
|
||||
"ImageBlankAlpha",
|
||||
@ -44768,6 +44880,7 @@
|
||||
"GetLatentSizeAndCount",
|
||||
"GetLatentsFromBatchIndexed",
|
||||
"GetMaskSizeAndCount",
|
||||
"GetPreviewOverrideFramesKJ",
|
||||
"GetTrackRange",
|
||||
"GradientToFloat",
|
||||
"GrowMaskWithBlur",
|
||||
@ -44813,7 +44926,6 @@
|
||||
"InsertImagesToBatchIndexed",
|
||||
"InsertLatentToIndexed",
|
||||
"InterpolateCoords",
|
||||
"Intrinsic_lora_sampling",
|
||||
"JoinStringMulti",
|
||||
"JoinStrings",
|
||||
"LTX2AttentionTunerPatch",
|
||||
@ -44844,6 +44956,7 @@
|
||||
"ModelMemoryUseReportPatch",
|
||||
"ModelPassThrough",
|
||||
"ModelPatchTorchSettings",
|
||||
"ModelPreviewOverrideKJ",
|
||||
"ModelSaveKJ",
|
||||
"NABLA_AttentionKJ",
|
||||
"NormalizedAmplitudeToFloatList",
|
||||
@ -51455,7 +51568,7 @@
|
||||
"SaveConditioning"
|
||||
],
|
||||
{
|
||||
"title_aux": "comfyui-conditioning-saver"
|
||||
"title_aux": "ComfyUI Conditioning Saver"
|
||||
}
|
||||
],
|
||||
"https://github.com/nickve28/ComfyUI-Nich-Utils": [
|
||||
@ -52440,6 +52553,7 @@
|
||||
"FunPackAdvisorLLM",
|
||||
"FunPackApplyLoraWeights",
|
||||
"FunPackClipVisionOutputCombine",
|
||||
"FunPackConditioningAdjust",
|
||||
"FunPackContextTransitionWindows",
|
||||
"FunPackContinueVideo",
|
||||
"FunPackDistilledFlowSampler",
|
||||
@ -52453,6 +52567,7 @@
|
||||
"FunPackStoryMemKeyframeExtractor",
|
||||
"FunPackStoryMemLastFrameExtractor",
|
||||
"FunPackStoryWriter",
|
||||
"FunPackStudio",
|
||||
"FunPackVideoRefinerV2",
|
||||
"FunPackVideoStitch"
|
||||
],
|
||||
@ -53666,6 +53781,19 @@
|
||||
"title_aux": "ComfyUI-Speaker-Isolation"
|
||||
}
|
||||
],
|
||||
"https://github.com/pmarmotte2/Comfyui_Pick_Any": [
|
||||
[
|
||||
"ADVAudioSeries",
|
||||
"ADVAudioSeries4",
|
||||
"ADVImageSeries4",
|
||||
"ADVPickAnyChooser",
|
||||
"ADVTextSeries",
|
||||
"ADVTextSeries4"
|
||||
],
|
||||
{
|
||||
"title_aux": "Comfyui-Pick_Any"
|
||||
}
|
||||
],
|
||||
"https://github.com/pnikolic-amd/ComfyUI_MIGraphX": [
|
||||
[
|
||||
"CompileDiffusersMIGraphX"
|
||||
@ -55328,9 +55456,11 @@
|
||||
[
|
||||
"RikanHiddenBase64ImageLoader",
|
||||
"RikanHiddenBase64ImageSaver",
|
||||
"RikanMultiLoraLoader",
|
||||
"RikanPromptRelayEncodeTimeline",
|
||||
"RikanPromptRelayMultiLoraGate",
|
||||
"RikanQwenCustomImageSize",
|
||||
"RikanRTXResolutionSettings",
|
||||
"RikanWanSpatioTemporalTiledVAEDecode",
|
||||
"rikan-i2vpainter",
|
||||
"rikan-i2vpainter-tiled-vae"
|
||||
|
||||
24
openapi.yaml
24
openapi.yaml
@ -259,6 +259,30 @@ paths:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/NodePackageMetadata'
|
||||
|
||||
/customnode/get_node_types_in_workflows:
|
||||
get:
|
||||
summary: List node types used by all user workflows
|
||||
description: Scan through all workflows in the Comfy user directory, and return a list of all node types used in each one.
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
workflow_file_name:
|
||||
type: string
|
||||
node_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
'500':
|
||||
description: Error occurred
|
||||
|
||||
/customnode/alternatives:
|
||||
get:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user