diff --git a/.cache/.cache_directory b/.cache/.cache_directory new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index 2670f77..49898d1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ .idea/ .vscode/ .tmp +.cache config.ini snapshots/** startup-scripts/** diff --git a/__init__.py b/__init__.py index d4e56b8..4c5b0cd 100644 --- a/__init__.py +++ b/__init__.py @@ -18,7 +18,7 @@ import re import signal import nodes -version = "V1.9" +version = "V1.10" print(f"### Loading: ComfyUI-Manager ({version})") required_comfyui_revision = 1793 @@ -96,6 +96,7 @@ custom_nodes_path = os.path.join(comfy_path, 'custom_nodes') js_path = os.path.join(comfy_path, "web", "extensions") comfyui_manager_path = os.path.dirname(__file__) +cache_dir = os.path.join(comfyui_manager_path, '.cache') local_db_model = os.path.join(comfyui_manager_path, "model-list.json") local_db_alter = os.path.join(comfyui_manager_path, "alter-list.json") local_db_custom_node_list = os.path.join(comfyui_manager_path, "custom-node-list.json") @@ -484,6 +485,44 @@ import zipfile import urllib.request +def simple_hash(input_string): + hash_value = 0 + for char in input_string: + hash_value = (hash_value * 31 + ord(char)) % (2**32) + + return hash_value + + +async def get_data_by_mode(mode, filename): + try: + if mode == "local": + uri = os.path.join(comfyui_manager_path, filename) + json_obj = await get_data(uri) + else: + uri = get_config()['channel_url'] + '/' + filename + cache_uri = str(simple_hash(uri))+'_'+filename + cache_uri = os.path.join(cache_dir, cache_uri) + + if mode == "cache": + if is_file_created_within_one_day(cache_uri): + json_obj = await get_data(cache_uri) + else: + json_obj = await get_data(uri) + with open(cache_uri, "w", encoding='utf-8') as file: + json.dump(json_obj, file, indent=4, sort_keys=True) + else: + uri = get_config()['channel_url'] + '/' + filename + json_obj = await get_data(uri) + with open(cache_uri, "w", encoding='utf-8') as file: + json.dump(json_obj, file, indent=4, sort_keys=True) + except Exception as e: + print(f"[ComfyUI-Manager] Due to a network error, switching to local mode.\n=> {filename}\n=> {e}") + uri = os.path.join(comfyui_manager_path, filename) + json_obj = await get_data(uri) + + return json_obj + + def get_model_dir(data): if data['save_path'] != 'default': if '..' in data['save_path'] or data['save_path'].startswith('/'): @@ -609,14 +648,20 @@ def check_custom_nodes_installed(json_obj, do_fetch=False, do_update_check=True, print(f"\x1b[2K\rUpdate check done.") +def is_file_created_within_one_day(file_path): + if not os.path.exists(file_path): + return False + + file_creation_time = os.path.getctime(file_path) + current_time = datetime.datetime.now().timestamp() + time_difference = current_time - file_creation_time + + return time_difference <= 86400 + + @server.PromptServer.instance.routes.get("/customnode/getmappings") async def fetch_customnode_mappings(request): - if request.rel_url.query["mode"] == "local": - uri = local_db_extension_node_mappings - else: - uri = get_config()['channel_url'] + '/extension-node-map.json' - - json_obj = await get_data(uri) + json_obj = await get_data_by_mode(request.rel_url.query["mode"], 'extension-node-map.json') all_nodes = set() patterns = [] @@ -639,12 +684,8 @@ async def fetch_customnode_mappings(request): @server.PromptServer.instance.routes.get("/customnode/fetch_updates") async def fetch_updates(request): try: - if request.rel_url.query["mode"] == "local": - uri = local_db_custom_node_list - else: - uri = get_config()['channel_url'] + '/custom-node-list.json' + json_obj = await get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') - json_obj = await get_data(uri) check_custom_nodes_installed(json_obj, True) update_exists = any('custom_nodes' in json_obj and 'installed' in node and node['installed'] == 'Update' for node in @@ -663,12 +704,8 @@ async def update_all(request): try: save_snapshot_with_postfix('autosave') - if request.rel_url.query["mode"] == "local": - uri = local_db_custom_node_list - else: - uri = get_config()['channel_url'] + '/custom-node-list.json' + json_obj = await get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') - json_obj = await get_data(uri) check_custom_nodes_installed(json_obj, do_update=True) update_exists = any(item['installed'] == 'Update' for item in json_obj['custom_nodes']) @@ -731,12 +768,11 @@ async def fetch_customnode_list(request): if request.rel_url.query["mode"] == "local": channel = 'local' - uri = local_db_custom_node_list else: channel = get_config()['channel_url'] - uri = channel + '/custom-node-list.json' - json_obj = await get_data(uri) + json_obj = await get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') + check_custom_nodes_installed(json_obj, False, not skip_update) for x in json_obj['custom_nodes']: @@ -766,15 +802,8 @@ async def fetch_alternatives_list(request): else: skip_update = False - if request.rel_url.query["mode"] == "local": - uri1 = local_db_alter - uri2 = local_db_custom_node_list - else: - uri1 = get_config()['channel_url'] + '/alter-list.json' - uri2 = get_config()['channel_url'] + '/custom-node-list.json' - - alter_json = await get_data(uri1) - custom_node_json = await get_data(uri2) + alter_json = await get_data_by_mode(request.rel_url.query["mode"], 'alter-list.json') + custom_node_json = await get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') fileurl_to_custom_node = {} @@ -813,12 +842,8 @@ def check_model_installed(json_obj): @server.PromptServer.instance.routes.get("/externalmodel/getlist") async def fetch_externalmodel_list(request): - if request.rel_url.query["mode"] == "local": - uri = local_db_model - else: - uri = get_config()['channel_url'] + '/model-list.json' + json_obj = await get_data_by_mode(request.rel_url.query["mode"], 'model-list.json') - json_obj = await get_data(uri) check_model_installed(json_obj) return web.json_response(json_obj, content_type='application/json') diff --git a/custom-node-list.json b/custom-node-list.json index af1946a..237f018 100644 --- a/custom-node-list.json +++ b/custom-node-list.json @@ -1498,7 +1498,7 @@ }, { "author": "rgthree", - "title": "rgthree's ComfyUi Nodes", + "title": "rgthree's ComfyUI Nodes", "reference": "https://github.com/rgthree/rgthree-comfy", "files": [ "https://github.com/rgthree/rgthree-comfy" @@ -3179,6 +3179,26 @@ "install_type": "git-clone", "description": "Nodes:FL Image Randomizer. The start of a pack that I will continue to build out to fill the gaps of nodes and functionality that I feel is missing in comfyUI" }, + { + "author": "zfkun", + "title": "ComfyUI_zfkun", + "reference": "https://github.com/zfkun/ComfyUI_zfkun", + "files": [ + "https://github.com/zfkun/ComfyUI_zfkun" + ], + "install_type": "git-clone", + "description": "Nodes: Preview Text, Text Translation." + }, + { + "author": "80sVectorz", + "title": "ComfyUI-Static-Primitives", + "reference": "https://github.com/80sVectorz/ComfyUI-Static-Primitives", + "files": [ + "https://github.com/80sVectorz/ComfyUI-Static-Primitives" + ], + "install_type": "git-clone", + "description": "Adds Static Primitives to ComfyUI. Mostly to work with reroute nodes" + }, { "author": "Ser-Hilary", "title": "SDXL_sizing", diff --git a/extension-node-map.json b/extension-node-map.json index 8ae906e..546c56f 100644 --- a/extension-node-map.json +++ b/extension-node-map.json @@ -1429,6 +1429,7 @@ "RerouteTextForCLIPTextEncodeA1111" ], { + "nodename_pattern": "- Ostris$", "title_aux": "ComfyUI A1111-like Prompt Custom Node Solution" } ], @@ -2730,7 +2731,8 @@ "https://github.com/chflame163/ComfyUI_MSSpeech_TTS": [ [ "MicorsoftSpeech_TTS", - "Play Sound" + "Play Sound", + "Play Sound (loop)" ], { "title_aux": "ComfyUI_MSSpeech_TTS" @@ -3506,6 +3508,9 @@ "https://github.com/jags111/ComfyUI_Jags_VectorMagic": [ [ "CircularVAEDecode", + "JagsCLIPSeg", + "JagsClipseg", + "JagsCombineMasks", "SVG", "YoloSEGdetectionNode", "YoloSegNode", @@ -4558,7 +4563,7 @@ "nickname": "rgthree", "nodename_pattern": " \\(rgthree\\)$", "title": "Comfy Nodes", - "title_aux": "rgthree's ComfyUi Nodes" + "title_aux": "rgthree's ComfyUI Nodes" } ], "https://github.com/richinsley/Comfy-LFO": [ @@ -5077,7 +5082,6 @@ "Random Line 4" ], { - "nodename_pattern": "\\(mtb\\)$", "title_aux": "Hakkun-ComfyUI-nodes" } ], @@ -5474,6 +5478,16 @@ "title_aux": "Cute Comfy" } ], + "https://github.com/zfkun/ComfyUI_zfkun": [ + [ + "ZFPreviewText", + "ZFPreviewTextMultiline", + "ZFTextTranslation" + ], + { + "title_aux": "ComfyUI_zfkun" + } + ], "https://github.com/zhuanqianfish/ComfyUI-EasyNode": [ [ "EasyCaptureNode", diff --git a/js/a1111-alter-downloader.js b/js/a1111-alter-downloader.js index 4f75ccb..adbf91c 100644 --- a/js/a1111-alter-downloader.js +++ b/js/a1111-alter-downloader.js @@ -4,9 +4,7 @@ import { ComfyDialog, $el } from "../../scripts/ui.js"; import { install_checked_custom_node, manager_instance, rebootAPI } from "./common.js"; async function getAlterList() { - var mode = "url"; - if(manager_instance.local_mode_checkbox.checked) - mode = "local"; + var mode = manager_instance.datasrc_combo.value; var skip_update = ""; if(manager_instance.update_check_checkbox.checked) diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index cce1c07..292e49e 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -224,7 +224,6 @@ async function init_notice(notice) { await init_badge_mode(); await init_share_option(); - async function fetchNicknames() { const response1 = await api.fetchApi(`/customnode/getmappings?mode=local`); const mappings = await response1.json(); @@ -235,7 +234,10 @@ async function fetchNicknames() { for (let i in mappings) { let item = mappings[i]; var nickname; - if (item[1].title) { + if (item[1].nickname) { + nickname = item[1].nickname; + } + else if (item[1].title) { nickname = item[1].title; } else { @@ -256,6 +258,84 @@ async function fetchNicknames() { const [nicknames, nickname_patterns] = await fetchNicknames(); +function getNickname(node, nodename) { + if(node.nickname) { + return node.nickname; + } + else { + if (nicknames[nodename]) { + node.nickname = nicknames[nodename]; + } + else { + for(let i in nickname_patterns) { + let item = nickname_patterns[i]; + if(nodename.match(item[0])) { + node.nickname = item[1]; + } + } + } + + return node.nickname; + } +} + +function drawBadge(node, orig, ctx) { + const r = orig?.apply?.(node, arguments); + + if (!node.flags.collapsed && badge_mode != 'none' && node.constructor.title_mode != LiteGraph.NO_TITLE) { + let text = ""; + if (badge_mode.startsWith('id_nick')) + text = `#${node.id} `; + + let nick = node.getNickname(); + if (nick) { + if (nick == 'ComfyUI') { + if(badge_mode.endsWith('hide')) { + nick = ""; + } + else { + nick = "🦊" + } + } + + if (nick.length > 25) { + text += nick.substring(0, 23) + ".."; + } + else { + text += nick; + } + } + + if (text != "") { + let fgColor = "white"; + let bgColor = "#0F1F0F"; + let visible = true; + + ctx.save(); + ctx.font = "12px sans-serif"; + const sz = ctx.measureText(text); + ctx.fillStyle = bgColor; + ctx.beginPath(); + ctx.roundRect(node.size[0] - sz.width - 12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); + ctx.fill(); + + ctx.fillStyle = fgColor; + ctx.fillText(text, node.size[0] - sz.width - 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); + ctx.restore(); + + if (node.has_errors) { + ctx.save(); + ctx.font = "bold 14px sans-serif"; + const sz2 = ctx.measureText(node.type); + ctx.fillStyle = 'white'; + ctx.fillText(node.type, node.size[0] / 2 - sz2.width / 2, node.size[1] / 2); + ctx.restore(); + } + } + } + return r; +} + async function updateComfyUI() { let prev_text = update_comfyui_button.innerText; @@ -302,9 +382,7 @@ async function fetchUpdates(update_check_checkbox) { fetch_updates_button.style.backgroundColor = "gray"; try { - var mode = "url"; - if(manager_instance.local_mode_checkbox.checked) - mode = "local"; + var mode = manager_instance.datasrc_combo.value; const response = await api.fetchApi(`/customnode/fetch_updates?mode=${mode}`); @@ -345,9 +423,7 @@ async function updateAll(update_check_checkbox, manager_dialog) { update_all_button.style.backgroundColor = "gray"; try { - var mode = "url"; - if(manager_instance.local_mode_checkbox.checked) - mode = "local"; + var mode = manager_instance.datasrc_combo.value; update_all_button.innerText = "Updating all..."; const response1 = await api.fetchApi('/comfyui_manager/update_comfyui'); @@ -417,8 +493,6 @@ const isOutputNode = (node) => { // ----------- class ManagerMenuDialog extends ComfyDialog { - local_mode_checkbox = null; - createControlsMid() { let self = this; @@ -505,18 +579,19 @@ class ManagerMenuDialog extends ComfyDialog { createControlsLeft() { let self = this; - this.local_mode_checkbox = $el("input",{type:'checkbox', id:"use_local_db"},[]) - const checkbox_text = $el("label",{for: "use_local_db"},[" Use local DB"]) - checkbox_text.style.color = "var(--fg-color)"; - checkbox_text.style.cursor = "pointer"; - checkbox_text.style.marginRight = "10px"; - this.update_check_checkbox = $el("input",{type:'checkbox', id:"skip_update_check"},[]) const uc_checkbox_text = $el("label",{for:"skip_update_check"},[" Skip update check"]) uc_checkbox_text.style.color = "var(--fg-color)"; uc_checkbox_text.style.cursor = "pointer"; this.update_check_checkbox.checked = true; + // db mode + this.datasrc_combo = document.createElement("select"); + this.datasrc_combo.style.cursor = "pointer"; + this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'DB: Cache (1day)' }, [])); + this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'DB: Local' }, [])); + this.datasrc_combo.appendChild($el('option', { value: 'url', text: 'DB: Remote' }, [])); + // preview method let preview_combo = document.createElement("select"); preview_combo.style.cursor = "pointer"; @@ -612,8 +687,9 @@ class ManagerMenuDialog extends ComfyDialog { }); return [ - $el("div", {}, [this.local_mode_checkbox, checkbox_text, this.update_check_checkbox, uc_checkbox_text]), + $el("div", {}, [this.update_check_checkbox, uc_checkbox_text]), $el("br", {}, []), + this.datasrc_combo, preview_combo, badge_combo, channel_combo, @@ -870,120 +946,21 @@ app.registerExtension({ }, async beforeRegisterNodeDef(nodeType, nodeData, app) { - const onDrawForeground = nodeType.prototype.onDrawForeground; - nodeType.prototype.onDrawForeground = function (ctx) { - const r = onDrawForeground?.apply?.(this, arguments); - - if (!this.flags.collapsed && badge_mode != 'none' && nodeType.title_mode != LiteGraph.NO_TITLE) { - let text = ""; - if (badge_mode.startsWith('id_nick')) - text = `#${this.id} `; - - if (nicknames[nodeData.name.trim()]) { - let nick = nicknames[nodeData.name.trim()]; - - if (nick == 'ComfyUI') { - if(badge_mode.endsWith('hide')) { - nick = ""; - } - else { - nick = "🦊" - } - } - - if (nick.length > 25) { - text += nick.substring(0, 23) + ".."; - } - else { - text += nick; - } - } - - if (text != "") { - let fgColor = "white"; - let bgColor = "#0F1F0F"; - let visible = true; - - ctx.save(); - ctx.font = "12px sans-serif"; - const sz = ctx.measureText(text); - ctx.fillStyle = bgColor; - ctx.beginPath(); - ctx.roundRect(this.size[0] - sz.width - 12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); - ctx.fill(); - - ctx.fillStyle = fgColor; - ctx.fillText(text, this.size[0] - sz.width - 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); - ctx.restore(); - } - } - return r; - }; - this._addExtraNodeContextMenu(nodeType, app); }, async nodeCreated(node, app) { - + if(!node.badge_enabled) { + node.getNickname = function () { return getNickname(node, node.comfyClass.trim()) }; + const orig = node.__proto__.onDrawForeground; + node.onDrawForeground = function (ctx) { drawBadge(node, orig, ctx) }; + node.badge_enabled = true; + } }, async loadedGraphNode(node, app) { - if (node.has_errors) { - const onDrawForeground = node.onDrawForeground; - node.onDrawForeground = function (ctx) { - const r = onDrawForeground?.apply?.(this, arguments); - - if (!this.flags.collapsed && badge_mode != 'none') { - let text = ""; - if (badge_mode.startsWith('id_nick')) - text = `#${this.id} `; - - if (nicknames[node.type.trim()]) { - let nick = nicknames[node.type.trim()]; - - if (nick == 'ComfyUI') { - if(badge_mode.endsWith('hide')) { - nick = ""; - } - else { - nick = "🦊" - } - } - - if (nick.length > 25) { - text += nick.substring(0, 23) + ".."; - } - else { - text += nick; - } - } - - if (text != "") { - let fgColor = "white"; - let bgColor = "#0F1F0F"; - let visible = true; - - ctx.save(); - ctx.font = "12px sans-serif"; - const sz = ctx.measureText(text); - ctx.fillStyle = bgColor; - ctx.beginPath(); - ctx.roundRect(this.size[0] - sz.width - 12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); - ctx.fill(); - - ctx.fillStyle = fgColor; - ctx.fillText(text, this.size[0] - sz.width - 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); - ctx.restore(); - - ctx.save(); - ctx.font = "bold 14px sans-serif"; - const sz2 = ctx.measureText(node.type); - ctx.fillStyle = 'white'; - ctx.fillText(node.type, this.size[0] / 2 - sz2.width / 2, this.size[1] / 2); - ctx.restore(); - } - } - - return r; - }; + if(!node.badge_enabled) { + const orig = node.onDrawForeground; + node.getNickname = function () { return getNickname(node, node.type.trim()) }; + node.onDrawForeground = function (ctx) { drawBadge(node, orig, ctx) }; } }, diff --git a/js/custom-nodes-downloader.js b/js/custom-nodes-downloader.js index 49e6ca7..25be09c 100644 --- a/js/custom-nodes-downloader.js +++ b/js/custom-nodes-downloader.js @@ -4,9 +4,7 @@ import { ComfyDialog, $el } from "../../scripts/ui.js"; import { install_checked_custom_node, manager_instance, rebootAPI } from "./common.js"; async function getCustomNodes() { - var mode = "url"; - if(manager_instance.local_mode_checkbox.checked) - mode = "local"; + var mode = manager_instance.datasrc_combo.value; var skip_update = ""; if(manager_instance.update_check_checkbox.checked) @@ -19,9 +17,7 @@ async function getCustomNodes() { } async function getCustomnodeMappings() { - var mode = "url"; - if(manager_instance.local_mode_checkbox.checked) - mode = "local"; + var mode = manager_instance.datasrc_combo.value; const response = await api.fetchApi(`/customnode/getmappings?mode=${mode}`); @@ -30,9 +26,7 @@ async function getCustomnodeMappings() { } async function getConflictMappings() { - var mode = "url"; - if(manager_instance.local_mode_checkbox.checked) - mode = "local"; + var mode = manager_instance.datasrc_combo.value; const response = await api.fetchApi(`/customnode/getmappings?mode=${mode}`); @@ -78,9 +72,7 @@ async function getConflictMappings() { async function getUnresolvedNodesInComponent() { try { - var mode = "url"; - if(manager_instance.local_mode_checkbox.checked) - mode = "local"; + var mode = manager_instance.datasrc_combo.value; const response = await api.fetchApi(`/component/get_unresolved`); diff --git a/js/model-downloader.js b/js/model-downloader.js index 9616ae7..65f54df 100644 --- a/js/model-downloader.js +++ b/js/model-downloader.js @@ -32,9 +32,7 @@ async function install_model(target) { } async function getModelList() { - var mode = "url"; - if(manager_instance.local_mode_checkbox.checked) - mode = "local"; + var mode = manager_instance.datasrc_combo.value; const response = await api.fetchApi(`/externalmodel/getlist?mode=${mode}`); diff --git a/node_db/new/custom-node-list.json b/node_db/new/custom-node-list.json index 3bda1d9..4478a17 100644 --- a/node_db/new/custom-node-list.json +++ b/node_db/new/custom-node-list.json @@ -1,5 +1,25 @@ { "custom_nodes": [ + { + "author": "80sVectorz", + "title": "ComfyUI-Static-Primitives", + "reference": "https://github.com/80sVectorz/ComfyUI-Static-Primitives", + "files": [ + "https://github.com/80sVectorz/ComfyUI-Static-Primitives" + ], + "install_type": "git-clone", + "description": "Adds Static Primitives to ComfyUI. Mostly to work with reroute nodes" + }, + { + "author": "zfkun", + "title": "ComfyUI_zfkun", + "reference": "https://github.com/zfkun/ComfyUI_zfkun", + "files": [ + "https://github.com/zfkun/ComfyUI_zfkun" + ], + "install_type": "git-clone", + "description": "Nodes: Preview Text, Text Translation." + }, { "author": "jtrue", "title": "ComfyUI-JaRue", diff --git a/node_db/new/extension-node-map.json b/node_db/new/extension-node-map.json index 8ae906e..546c56f 100644 --- a/node_db/new/extension-node-map.json +++ b/node_db/new/extension-node-map.json @@ -1429,6 +1429,7 @@ "RerouteTextForCLIPTextEncodeA1111" ], { + "nodename_pattern": "- Ostris$", "title_aux": "ComfyUI A1111-like Prompt Custom Node Solution" } ], @@ -2730,7 +2731,8 @@ "https://github.com/chflame163/ComfyUI_MSSpeech_TTS": [ [ "MicorsoftSpeech_TTS", - "Play Sound" + "Play Sound", + "Play Sound (loop)" ], { "title_aux": "ComfyUI_MSSpeech_TTS" @@ -3506,6 +3508,9 @@ "https://github.com/jags111/ComfyUI_Jags_VectorMagic": [ [ "CircularVAEDecode", + "JagsCLIPSeg", + "JagsClipseg", + "JagsCombineMasks", "SVG", "YoloSEGdetectionNode", "YoloSegNode", @@ -4558,7 +4563,7 @@ "nickname": "rgthree", "nodename_pattern": " \\(rgthree\\)$", "title": "Comfy Nodes", - "title_aux": "rgthree's ComfyUi Nodes" + "title_aux": "rgthree's ComfyUI Nodes" } ], "https://github.com/richinsley/Comfy-LFO": [ @@ -5077,7 +5082,6 @@ "Random Line 4" ], { - "nodename_pattern": "\\(mtb\\)$", "title_aux": "Hakkun-ComfyUI-nodes" } ], @@ -5474,6 +5478,16 @@ "title_aux": "Cute Comfy" } ], + "https://github.com/zfkun/ComfyUI_zfkun": [ + [ + "ZFPreviewText", + "ZFPreviewTextMultiline", + "ZFTextTranslation" + ], + { + "title_aux": "ComfyUI_zfkun" + } + ], "https://github.com/zhuanqianfish/ComfyUI-EasyNode": [ [ "EasyCaptureNode",