Browse Source

feat: badge feature

bugfix: robust uninstall of .js
improve: scanner.py crawl metadata from docstring
pull/68/head
dr.lt.data 1 year ago committed by Dr.Lt.Data
parent
commit
1a2c673660
  1. 19
      README.md
  2. 31
      __init__.py
  3. 20
      custom-node-list.json
  4. 3116
      extension-node-map.json
  5. 136
      js/comfyui-manager.js
  6. BIN
      misc/menu.jpg
  7. BIN
      misc/nickname.jpg
  8. 52
      scanner.py

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.
![menu](misc/menu.jpg)
* 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.
![model-install-dialog](misc/nickname.jpg)
## 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

@ -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']

20
custom-node-list.json

@ -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",

3116
extension-node-map.json

File diff suppressed because it is too large Load Diff

136
js/comfyui-manager.js vendored

@ -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)
@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 52 KiB

BIN
misc/nickname.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

52
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:

Loading…
Cancel
Save