mirror of
https://git.datalinker.icu/ltdrdata/ComfyUI-Manager
synced 2025-12-08 21:54:26 +08:00
feat: badge feature
bugfix: robust uninstall of .js improve: scanner.py crawl metadata from docstring
This commit is contained in:
parent
1449acd4dc
commit
1a2c673660
19
README.md
19
README.md
@ -65,6 +65,7 @@ This repository provides Colab notebooks that allow you to install and use Comfy
|
||||
|
||||
|
||||
2. If you click on 'Install Custom Nodes' or 'Install Models', an installer dialog will open.
|
||||
|
||||

|
||||
|
||||
* When the 'Use local DB' feature is enabled, the application will utilize the data stored locally on your device, rather than retrieving node/model information over the internet
|
||||
@ -81,6 +82,11 @@ This repository provides Colab notebooks that allow you to install and use Comfy
|
||||
* Install: Clicking this button will install the item.
|
||||
* Try Install: This is a custom node of which installation information cannot be confirmed. Click the button to try installing it.
|
||||
|
||||
4. If you set the `Badge:` item in the menu as `Badge: Nickname` or `Badge: #ID Nickname`, the information badge will be displayed on the node.
|
||||
* `Badge: Nickname` displays the nickname of custom nodes, while `Badge: ID Nickname` also includes the internal ID of the node.
|
||||
|
||||

|
||||
|
||||
|
||||
## Custom node support guide
|
||||
|
||||
@ -104,6 +110,19 @@ NODE_CLASS_MAPPINGS.update({
|
||||
})
|
||||
```
|
||||
|
||||
* When you write a docstring in the header of the .py file for the Node as follows, it will be used for managing the database in the Manager.
|
||||
* Currently, only the `nickname` is being used, but other parts will also be utilized in the future.
|
||||
* The `nickname` will be the name displayed on the badge of the node.
|
||||
* If there is no `nickname`, it will be truncated to 20 characters from the arbitrarily written title and used.
|
||||
```
|
||||
"""
|
||||
@author: Dr.Lt.Data
|
||||
@title: Impact Pack
|
||||
@nickname: Impact Pack
|
||||
@description: This extension offers various detector nodes and detailer nodes that allow you to configure a workflow that automatically enhances facial details. And provide iterative upscaler.
|
||||
"""
|
||||
```
|
||||
|
||||
* **Special purpose files** (optional)
|
||||
* `node_list.js` - When your custom nodes pattern of NODE_CLASS_MAPPINGS is not conventional, it is used to manually provide a list of nodes for reference. ([example](https://github.com/melMass/comfy_mtb/raw/main/node_list.json))
|
||||
* `requirements.txt` - When installing, this pip requirements will be installed automatically
|
||||
|
||||
31
__init__.py
31
__init__.py
@ -55,7 +55,7 @@ sys.path.append('../..')
|
||||
from torchvision.datasets.utils import download_url
|
||||
|
||||
# ensure .js
|
||||
print("### Loading: ComfyUI-Manager (V0.21.4)")
|
||||
print("### Loading: ComfyUI-Manager (V0.22)")
|
||||
|
||||
comfy_ui_required_revision = 1240
|
||||
comfy_ui_revision = "Unknown"
|
||||
@ -84,6 +84,7 @@ def write_config():
|
||||
config = configparser.ConfigParser()
|
||||
config['default'] = {
|
||||
'preview_method': get_current_preview_method(),
|
||||
'badge_mode': get_config()['badge_mode']
|
||||
}
|
||||
with open(config_path, 'w') as configfile:
|
||||
config.write(configfile)
|
||||
@ -96,11 +97,12 @@ def read_config():
|
||||
default_conf = config['default']
|
||||
|
||||
return {
|
||||
'preview_method': default_conf['preview_method']
|
||||
'preview_method': default_conf['preview_method'] if 'preview_method' in default_conf else get_current_preview_method(),
|
||||
'badge_mode': default_conf['badge_mode'] if 'badge_mode' in default_conf else 'none'
|
||||
}
|
||||
|
||||
except Exception:
|
||||
return {'preview_method': get_current_preview_method()}
|
||||
return {'preview_method': get_current_preview_method(), 'badge_mode': 'none'}
|
||||
|
||||
|
||||
def get_config():
|
||||
@ -136,6 +138,10 @@ def set_preview_method(method):
|
||||
get_config()['preview_method'] = args.preview_method
|
||||
|
||||
|
||||
def set_badge_mode(mode):
|
||||
get_config()['badge_mode'] = mode
|
||||
|
||||
|
||||
set_preview_method(get_config()['preview_method'])
|
||||
|
||||
|
||||
@ -588,7 +594,7 @@ def copy_install(files, js_path_name=None):
|
||||
return True
|
||||
|
||||
|
||||
def copy_uninstall(files, js_path_name=None):
|
||||
def copy_uninstall(files, js_path_name='.'):
|
||||
for url in files:
|
||||
dir_name = os.path.basename(url)
|
||||
base_path = custom_nodes_path if url.endswith('.py') else os.path.join(js_path, js_path_name)
|
||||
@ -607,7 +613,7 @@ def copy_uninstall(files, js_path_name=None):
|
||||
return True
|
||||
|
||||
|
||||
def copy_set_active(files, is_disable, js_path_name=None):
|
||||
def copy_set_active(files, is_disable, js_path_name='.'):
|
||||
if is_disable:
|
||||
action_name = "Disable"
|
||||
else:
|
||||
@ -838,7 +844,7 @@ async def install_custom_node(request):
|
||||
res = unzip_install(json_data['files'])
|
||||
|
||||
if install_type == "copy":
|
||||
js_path_name = json_data['js_path'] if 'js_path' in json_data else None
|
||||
js_path_name = json_data['js_path'] if 'js_path' in json_data else '.'
|
||||
res = copy_install(json_data['files'], js_path_name)
|
||||
|
||||
elif install_type == "git-clone":
|
||||
@ -867,7 +873,7 @@ async def install_custom_node(request):
|
||||
res = False
|
||||
|
||||
if install_type == "copy":
|
||||
js_path_name = json_data['js_path'] if 'js_path' in json_data else None
|
||||
js_path_name = json_data['js_path'] if 'js_path' in json_data else '.'
|
||||
res = copy_uninstall(json_data['files'], js_path_name)
|
||||
|
||||
elif install_type == "git-clone":
|
||||
@ -1000,5 +1006,16 @@ async def preview_method(request):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/manager/badge_mode")
|
||||
async def badge_mode(request):
|
||||
if "value" in request.rel_url.query:
|
||||
set_badge_mode(request.rel_url.query['value'])
|
||||
write_config()
|
||||
else:
|
||||
return web.Response(text=get_config()['badge_mode'], status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
__all__ = ['NODE_CLASS_MAPPINGS']
|
||||
|
||||
@ -1109,6 +1109,26 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: NoisyLatentPerlin. This allows to create latent spaces filled with perlin-based noise that can actually be used by the samplers."
|
||||
},
|
||||
{
|
||||
"author": "JPS-GER",
|
||||
"title": "JPS Custom Nodes for ComfyUI",
|
||||
"reference": "https://github.com/JPS-GER/ComfyUI_JPS-Nodes",
|
||||
"files": [
|
||||
"https://github.com/JPS-GER/ComfyUI_JPS-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: SDXL - Resolutions, SDXL - Basic Settings, SDXL - Additional Settings, Math - Resolution Multiply, Math - Largest Integer, Switch - Generation Mode, ..."
|
||||
},
|
||||
{
|
||||
"author": "hustille",
|
||||
"title": "hus' utils for ComfyUI",
|
||||
"reference": "https://github.com/hustille/ComfyUI_hus_utils",
|
||||
"files": [
|
||||
"https://github.com/hustille/ComfyUI_hus_utils"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: Fetch widget value, 3way Prompt Styler, Text Hash, Date Time Format, Batch State, Debug Extra, ..."
|
||||
},
|
||||
{
|
||||
"author": "taabata",
|
||||
"title": "Syrian Falcon Nodes",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,15 @@ import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||
|
||||
var update_comfyui_button = null;
|
||||
var fetch_updates_button = null;
|
||||
var badge_mode = "none";
|
||||
|
||||
async function init_badge_mode() {
|
||||
api.fetchApi('/manager/badge_mode')
|
||||
.then(response => response.text())
|
||||
.then(data => { badge_mode = data; })
|
||||
}
|
||||
|
||||
await init_badge_mode();
|
||||
|
||||
async function getCustomnodeMappings() {
|
||||
var mode = "url";
|
||||
@ -48,6 +57,32 @@ async function getCustomNodes() {
|
||||
return data;
|
||||
}
|
||||
|
||||
async function fetchNicknames() {
|
||||
const response1 = await api.fetchApi(`/customnode/getmappings?mode=local`);
|
||||
const mappings = await response1.json();
|
||||
|
||||
let result = {};
|
||||
|
||||
for(let i in mappings) {
|
||||
let item = mappings[i];
|
||||
var nickname;
|
||||
if(item[1].title) {
|
||||
nickname = item[1].title;
|
||||
}
|
||||
else {
|
||||
nickname = item[1].title_aux;
|
||||
}
|
||||
|
||||
for(let j in item[0]) {
|
||||
result[item[0][j]] = nickname;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
let nicknames = await fetchNicknames();
|
||||
|
||||
async function getAlterList() {
|
||||
var mode = "url";
|
||||
if(ManagerMenuDialog.instance.local_mode_checkbox.checked)
|
||||
@ -89,7 +124,7 @@ async function install_custom_node(target, caller, mode) {
|
||||
app.ui.dialog.show(`${mode} failed: ${target.title}`);
|
||||
app.ui.dialog.element.style.zIndex = 9999;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const status = await response.json();
|
||||
app.ui.dialog.close();
|
||||
@ -238,7 +273,7 @@ class CustomNodesInstaller extends ComfyDialog {
|
||||
|
||||
startInstall(target) {
|
||||
const self = CustomNodesInstaller.instance;
|
||||
|
||||
|
||||
self.updateMessage(`<BR><font color="green">Installing '${target.title}'</font>`);
|
||||
|
||||
for(let i in self.install_buttons) {
|
||||
@ -335,8 +370,8 @@ class CustomNodesInstaller extends ComfyDialog {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
}
|
||||
|
||||
const msg = $el('div', {id:'custom-message'},
|
||||
[$el('br'),
|
||||
const msg = $el('div', {id:'custom-message'},
|
||||
[$el('br'),
|
||||
'The custom node DB is currently being updated, and updates to custom nodes are being checked for.',
|
||||
$el('br'),
|
||||
'NOTE: Update only checks for extensions that have been fetched.',
|
||||
@ -1368,11 +1403,12 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
() => fetchUpdates(this.update_check_checkbox)
|
||||
});
|
||||
|
||||
// preview method
|
||||
let preview_combo = document.createElement("select");
|
||||
preview_combo.appendChild($el('option', {value:'auto', text:'Preview method: Auto'}, []));
|
||||
preview_combo.appendChild($el('option', {value:'taesd', text:'Preview method: TAESD'}, []));
|
||||
preview_combo.appendChild($el('option', {value:'latent2rgb', text:'Preview method: Latent2RGB'}, []));
|
||||
preview_combo.appendChild($el('option', {value:'none', text:'Preview method: None'}, []));
|
||||
preview_combo.appendChild($el('option', {value:'taesd', text:'Preview method: TAESD (slow)'}, []));
|
||||
preview_combo.appendChild($el('option', {value:'latent2rgb', text:'Preview method: Latent2RGB (fast)'}, []));
|
||||
preview_combo.appendChild($el('option', {value:'none', text:'Preview method: None (very fast)'}, []));
|
||||
|
||||
api.fetchApi('/manager/preview_method')
|
||||
.then(response => response.text())
|
||||
@ -1382,6 +1418,22 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
api.fetchApi(`/manager/preview_method?value=${event.target.value}`);
|
||||
});
|
||||
|
||||
// nickname
|
||||
let badge_combo = document.createElement("select");
|
||||
badge_combo.appendChild($el('option', {value:'none', text:'Badge: None'}, []));
|
||||
badge_combo.appendChild($el('option', {value:'nick', text:'Badge: Nickname'}, []));
|
||||
badge_combo.appendChild($el('option', {value:'id_nick', text:'Badge: #ID Nickname'}, []));
|
||||
|
||||
api.fetchApi('/manager/badge_mode')
|
||||
.then(response => response.text())
|
||||
.then(data => { badge_combo.value = data; badge_mode = data; })
|
||||
|
||||
badge_combo.addEventListener('change', function(event) {
|
||||
api.fetchApi(`/manager/badge_mode?value=${event.target.value}`);
|
||||
badge_mode = event.target.value;
|
||||
app.graph.setDirtyCanvas(true);
|
||||
});
|
||||
|
||||
const res =
|
||||
[
|
||||
$el("tr.td", {width:"100%"}, [$el("font", {size:6, color:"white"}, [`ComfyUI Manager Menu`])]),
|
||||
@ -1447,6 +1499,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
$el("br", {}, []),
|
||||
$el("hr", {width: "100%"}, []),
|
||||
preview_combo,
|
||||
badge_combo,
|
||||
$el("hr", {width: "100%"}, []),
|
||||
$el("br", {}, []),
|
||||
|
||||
@ -1497,5 +1550,82 @@ app.registerExtension({
|
||||
ManagerMenuDialog.instance.show();
|
||||
}
|
||||
menu.append(managerButton);
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if(nicknames[nodeData.name.trim()]) {
|
||||
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
||||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||
const r = onDrawForeground?.apply?.(this, arguments);
|
||||
|
||||
if(badge_mode != 'none') {
|
||||
let text = nicknames[nodeData.name.trim()];
|
||||
if(text.length > 18) {
|
||||
text = text.substring(0,17)+"..";
|
||||
}
|
||||
|
||||
if(badge_mode == 'id_nick')
|
||||
text = `#${this.id} ${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;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async loadedGraphNode(node, app) {
|
||||
if(node.has_errors) {
|
||||
if(nicknames[node.type.trim()]) {
|
||||
const onDrawForeground = node.onDrawForeground;
|
||||
node.onDrawForeground = function (ctx) {
|
||||
const r = onDrawForeground?.apply?.(this, arguments);
|
||||
|
||||
if(badge_mode != 'none') {
|
||||
let text = nicknames[node.type.trim()];
|
||||
|
||||
if(text.length > 18) {
|
||||
text = text.substring(0,17)+"..";
|
||||
}
|
||||
|
||||
if(badge_mode == 'id_nick')
|
||||
text = `#${this.id} ${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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
BIN
misc/menu.jpg
BIN
misc/menu.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 52 KiB |
BIN
misc/nickname.jpg
Normal file
BIN
misc/nickname.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
54
scanner.py
54
scanner.py
@ -53,7 +53,15 @@ def scan_in_file(filename):
|
||||
class_dict[key.strip()] = value.strip()
|
||||
nodes.add(key.strip())
|
||||
|
||||
return nodes
|
||||
metadata = {}
|
||||
lines = code.strip().split('\n')
|
||||
for line in lines:
|
||||
if line.startswith('@'):
|
||||
if line.startswith("@author:") or line.startswith("@title:") or line.startswith("@nickname:") or line.startswith("@description:"):
|
||||
key, value = line[1:].strip().split(':')
|
||||
metadata[key.strip()] = value.strip()
|
||||
|
||||
return nodes, metadata
|
||||
|
||||
def get_py_file_paths(dirname):
|
||||
file_paths = []
|
||||
@ -98,7 +106,7 @@ def get_git_urls_from_json(json_file):
|
||||
if node.get('install_type') == 'git-clone':
|
||||
files = node.get('files', [])
|
||||
if files:
|
||||
git_clone_files.append(files[0])
|
||||
git_clone_files.append((files[0],node.get('title')))
|
||||
|
||||
return git_clone_files
|
||||
|
||||
@ -113,7 +121,7 @@ def get_py_urls_from_json(json_file):
|
||||
if node.get('install_type') == 'copy':
|
||||
files = node.get('files', [])
|
||||
if files:
|
||||
py_files.append(files[0])
|
||||
py_files.append((files[0],node.get('title')))
|
||||
|
||||
return py_files
|
||||
|
||||
@ -146,22 +154,22 @@ def update_custom_nodes():
|
||||
|
||||
node_info = {}
|
||||
|
||||
git_urls = get_git_urls_from_json('custom-node-list.json')
|
||||
git_url_titles = get_git_urls_from_json('custom-node-list.json')
|
||||
|
||||
for url in git_urls:
|
||||
for url, title in git_url_titles:
|
||||
name = os.path.basename(url)
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
|
||||
node_info[name] = url
|
||||
node_info[name] = (url, title)
|
||||
clone_or_pull_git_repository(url)
|
||||
|
||||
py_urls = get_py_urls_from_json('custom-node-list.json')
|
||||
py_url_titles = get_py_urls_from_json('custom-node-list.json')
|
||||
|
||||
for url in py_urls:
|
||||
for url, title in py_url_titles:
|
||||
name = os.path.basename(url)
|
||||
if name.endswith(".py"):
|
||||
node_info[name] = url
|
||||
node_info[name] = (url, title)
|
||||
|
||||
try:
|
||||
download_url(url, ".tmp")
|
||||
@ -178,10 +186,13 @@ def gen_json(node_info):
|
||||
data = {}
|
||||
for dirname in node_dirs:
|
||||
py_files = get_py_file_paths(dirname)
|
||||
metadata = {}
|
||||
|
||||
nodes = set()
|
||||
for py in py_files:
|
||||
nodes.update(scan_in_file(py))
|
||||
nodes_in_file, metadata_in_file = scan_in_file(py)
|
||||
nodes.update(nodes_in_file)
|
||||
metadata.update(metadata_in_file)
|
||||
|
||||
dirname = os.path.basename(dirname)
|
||||
|
||||
@ -190,13 +201,14 @@ def gen_json(node_info):
|
||||
nodes.sort()
|
||||
|
||||
if dirname in node_info:
|
||||
git_url = node_info[dirname]
|
||||
data[git_url] = nodes
|
||||
git_url, title = node_info[dirname]
|
||||
metadata['title_aux'] = title
|
||||
data[git_url] = (nodes, metadata)
|
||||
else:
|
||||
print(f"WARN: {dirname} is removed from custom-node-list.json")
|
||||
|
||||
for file in node_files:
|
||||
nodes = scan_in_file(file)
|
||||
nodes, metadata = scan_in_file(file)
|
||||
|
||||
if len(nodes) > 0:
|
||||
nodes = list(nodes)
|
||||
@ -205,8 +217,9 @@ def gen_json(node_info):
|
||||
file = os.path.basename(file)
|
||||
|
||||
if file in node_info:
|
||||
url = node_info[file]
|
||||
data[url] = nodes
|
||||
url, title = node_info[file]
|
||||
metadata['title_aux'] = title
|
||||
data[url] = (nodes, metadata)
|
||||
else:
|
||||
print(f"Missing info: {url}")
|
||||
|
||||
@ -216,22 +229,25 @@ def gen_json(node_info):
|
||||
for extension in extensions:
|
||||
node_list_json_path = os.path.join('.tmp', extension, 'node_list.json')
|
||||
if os.path.exists(node_list_json_path):
|
||||
git_url = node_info[extension]
|
||||
git_url, title = node_info[extension]
|
||||
|
||||
with open(node_list_json_path, 'r') as f:
|
||||
node_list_json = json.load(f)
|
||||
|
||||
metadata_in_url = {}
|
||||
if git_url not in data:
|
||||
nodes = set()
|
||||
else:
|
||||
nodes = set(data[git_url])
|
||||
nodes_in_url, metadata_in_url = data[git_url]
|
||||
nodes = set(nodes_in_url)
|
||||
|
||||
for x, desc in node_list_json.items():
|
||||
nodes.add(x.strip())
|
||||
|
||||
metadata_in_url['title_aux'] = title
|
||||
nodes = list(nodes)
|
||||
nodes.sort()
|
||||
data[git_url] = nodes
|
||||
data[git_url] = (nodes, metadata_in_url)
|
||||
|
||||
json_path = f"extension-node-map.json"
|
||||
with open(json_path, "w") as file:
|
||||
@ -244,4 +260,4 @@ print("\n# Updating extensions\n")
|
||||
updated_node_info = update_custom_nodes()
|
||||
|
||||
print("\n# 'extension-node-map.json' file is generated.\n")
|
||||
gen_json(updated_node_info)
|
||||
gen_json(updated_node_info)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user