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. 144
      js/comfyui-manager.js
  6. BIN
      misc/menu.jpg
  7. BIN
      misc/nickname.jpg
  8. 54
      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. 2. If you click on 'Install Custom Nodes' or 'Install Models', an installer dialog will open.
![menu](misc/menu.jpg) ![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 * 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. * 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. * 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 ## 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) * **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)) * `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 * `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 from torchvision.datasets.utils import download_url
# ensure .js # ensure .js
print("### Loading: ComfyUI-Manager (V0.21.4)") print("### Loading: ComfyUI-Manager (V0.22)")
comfy_ui_required_revision = 1240 comfy_ui_required_revision = 1240
comfy_ui_revision = "Unknown" comfy_ui_revision = "Unknown"
@ -84,6 +84,7 @@ def write_config():
config = configparser.ConfigParser() config = configparser.ConfigParser()
config['default'] = { config['default'] = {
'preview_method': get_current_preview_method(), 'preview_method': get_current_preview_method(),
'badge_mode': get_config()['badge_mode']
} }
with open(config_path, 'w') as configfile: with open(config_path, 'w') as configfile:
config.write(configfile) config.write(configfile)
@ -96,11 +97,12 @@ def read_config():
default_conf = config['default'] default_conf = config['default']
return { 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: except Exception:
return {'preview_method': get_current_preview_method()} return {'preview_method': get_current_preview_method(), 'badge_mode': 'none'}
def get_config(): def get_config():
@ -136,6 +138,10 @@ def set_preview_method(method):
get_config()['preview_method'] = args.preview_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']) set_preview_method(get_config()['preview_method'])
@ -588,7 +594,7 @@ def copy_install(files, js_path_name=None):
return True return True
def copy_uninstall(files, js_path_name=None): def copy_uninstall(files, js_path_name='.'):
for url in files: for url in files:
dir_name = os.path.basename(url) dir_name = os.path.basename(url)
base_path = custom_nodes_path if url.endswith('.py') else os.path.join(js_path, js_path_name) 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 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: if is_disable:
action_name = "Disable" action_name = "Disable"
else: else:
@ -838,7 +844,7 @@ async def install_custom_node(request):
res = unzip_install(json_data['files']) res = unzip_install(json_data['files'])
if install_type == "copy": 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) res = copy_install(json_data['files'], js_path_name)
elif install_type == "git-clone": elif install_type == "git-clone":
@ -867,7 +873,7 @@ async def install_custom_node(request):
res = False res = False
if install_type == "copy": 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) res = copy_uninstall(json_data['files'], js_path_name)
elif install_type == "git-clone": elif install_type == "git-clone":
@ -1000,5 +1006,16 @@ async def preview_method(request):
return web.Response(status=200) 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 = {} NODE_CLASS_MAPPINGS = {}
__all__ = ['NODE_CLASS_MAPPINGS'] __all__ = ['NODE_CLASS_MAPPINGS']

20
custom-node-list.json

@ -1109,6 +1109,26 @@
"install_type": "git-clone", "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." "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", "author": "taabata",
"title": "Syrian Falcon Nodes", "title": "Syrian Falcon Nodes",

3116
extension-node-map.json

File diff suppressed because it is too large Load Diff

144
js/comfyui-manager.js vendored

@ -5,6 +5,15 @@ import {ComfyWidgets} from "../../scripts/widgets.js";
var update_comfyui_button = null; var update_comfyui_button = null;
var fetch_updates_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() { async function getCustomnodeMappings() {
var mode = "url"; var mode = "url";
@ -48,6 +57,32 @@ async function getCustomNodes() {
return data; 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() { async function getAlterList() {
var mode = "url"; var mode = "url";
if(ManagerMenuDialog.instance.local_mode_checkbox.checked) 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.show(`${mode} failed: ${target.title}`);
app.ui.dialog.element.style.zIndex = 9999; app.ui.dialog.element.style.zIndex = 9999;
return false; return false;
} }
const status = await response.json(); const status = await response.json();
app.ui.dialog.close(); app.ui.dialog.close();
@ -238,7 +273,7 @@ class CustomNodesInstaller extends ComfyDialog {
startInstall(target) { startInstall(target) {
const self = CustomNodesInstaller.instance; const self = CustomNodesInstaller.instance;
self.updateMessage(`<BR><font color="green">Installing '${target.title}'</font>`); self.updateMessage(`<BR><font color="green">Installing '${target.title}'</font>`);
for(let i in self.install_buttons) { for(let i in self.install_buttons) {
@ -335,8 +370,8 @@ class CustomNodesInstaller extends ComfyDialog {
this.element.removeChild(this.element.children[0]); this.element.removeChild(this.element.children[0]);
} }
const msg = $el('div', {id:'custom-message'}, const msg = $el('div', {id:'custom-message'},
[$el('br'), [$el('br'),
'The custom node DB is currently being updated, and updates to custom nodes are being checked for.', 'The custom node DB is currently being updated, and updates to custom nodes are being checked for.',
$el('br'), $el('br'),
'NOTE: Update only checks for extensions that have been fetched.', 'NOTE: Update only checks for extensions that have been fetched.',
@ -1368,11 +1403,12 @@ class ManagerMenuDialog extends ComfyDialog {
() => fetchUpdates(this.update_check_checkbox) () => fetchUpdates(this.update_check_checkbox)
}); });
// preview method
let preview_combo = document.createElement("select"); let preview_combo = document.createElement("select");
preview_combo.appendChild($el('option', {value:'auto', text:'Preview method: Auto'}, [])); 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:'taesd', text:'Preview method: TAESD (slow)'}, []));
preview_combo.appendChild($el('option', {value:'latent2rgb', text:'Preview method: Latent2RGB'}, [])); preview_combo.appendChild($el('option', {value:'latent2rgb', text:'Preview method: Latent2RGB (fast)'}, []));
preview_combo.appendChild($el('option', {value:'none', text:'Preview method: None'}, [])); preview_combo.appendChild($el('option', {value:'none', text:'Preview method: None (very fast)'}, []));
api.fetchApi('/manager/preview_method') api.fetchApi('/manager/preview_method')
.then(response => response.text()) .then(response => response.text())
@ -1382,6 +1418,22 @@ class ManagerMenuDialog extends ComfyDialog {
api.fetchApi(`/manager/preview_method?value=${event.target.value}`); 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 = const res =
[ [
$el("tr.td", {width:"100%"}, [$el("font", {size:6, color:"white"}, [`ComfyUI Manager Menu`])]), $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("br", {}, []),
$el("hr", {width: "100%"}, []), $el("hr", {width: "100%"}, []),
preview_combo, preview_combo,
badge_combo,
$el("hr", {width: "100%"}, []), $el("hr", {width: "100%"}, []),
$el("br", {}, []), $el("br", {}, []),
@ -1497,5 +1550,82 @@ app.registerExtension({
ManagerMenuDialog.instance.show(); ManagerMenuDialog.instance.show();
} }
menu.append(managerButton); 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

54
scanner.py

@ -53,7 +53,15 @@ def scan_in_file(filename):
class_dict[key.strip()] = value.strip() class_dict[key.strip()] = value.strip()
nodes.add(key.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): def get_py_file_paths(dirname):
file_paths = [] file_paths = []
@ -98,7 +106,7 @@ def get_git_urls_from_json(json_file):
if node.get('install_type') == 'git-clone': if node.get('install_type') == 'git-clone':
files = node.get('files', []) files = node.get('files', [])
if files: if files:
git_clone_files.append(files[0]) git_clone_files.append((files[0],node.get('title')))
return git_clone_files return git_clone_files
@ -113,7 +121,7 @@ def get_py_urls_from_json(json_file):
if node.get('install_type') == 'copy': if node.get('install_type') == 'copy':
files = node.get('files', []) files = node.get('files', [])
if files: if files:
py_files.append(files[0]) py_files.append((files[0],node.get('title')))
return py_files return py_files
@ -146,22 +154,22 @@ def update_custom_nodes():
node_info = {} 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) name = os.path.basename(url)
if name.endswith(".git"): if name.endswith(".git"):
name = name[:-4] name = name[:-4]
node_info[name] = url node_info[name] = (url, title)
clone_or_pull_git_repository(url) 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) name = os.path.basename(url)
if name.endswith(".py"): if name.endswith(".py"):
node_info[name] = url node_info[name] = (url, title)
try: try:
download_url(url, ".tmp") download_url(url, ".tmp")
@ -178,10 +186,13 @@ def gen_json(node_info):
data = {} data = {}
for dirname in node_dirs: for dirname in node_dirs:
py_files = get_py_file_paths(dirname) py_files = get_py_file_paths(dirname)
metadata = {}
nodes = set() nodes = set()
for py in py_files: 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) dirname = os.path.basename(dirname)
@ -190,13 +201,14 @@ def gen_json(node_info):
nodes.sort() nodes.sort()
if dirname in node_info: if dirname in node_info:
git_url = node_info[dirname] git_url, title = node_info[dirname]
data[git_url] = nodes metadata['title_aux'] = title
data[git_url] = (nodes, metadata)
else: else:
print(f"WARN: {dirname} is removed from custom-node-list.json") print(f"WARN: {dirname} is removed from custom-node-list.json")
for file in node_files: for file in node_files:
nodes = scan_in_file(file) nodes, metadata = scan_in_file(file)
if len(nodes) > 0: if len(nodes) > 0:
nodes = list(nodes) nodes = list(nodes)
@ -205,8 +217,9 @@ def gen_json(node_info):
file = os.path.basename(file) file = os.path.basename(file)
if file in node_info: if file in node_info:
url = node_info[file] url, title = node_info[file]
data[url] = nodes metadata['title_aux'] = title
data[url] = (nodes, metadata)
else: else:
print(f"Missing info: {url}") print(f"Missing info: {url}")
@ -216,22 +229,25 @@ def gen_json(node_info):
for extension in extensions: for extension in extensions:
node_list_json_path = os.path.join('.tmp', extension, 'node_list.json') node_list_json_path = os.path.join('.tmp', extension, 'node_list.json')
if os.path.exists(node_list_json_path): 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: with open(node_list_json_path, 'r') as f:
node_list_json = json.load(f) node_list_json = json.load(f)
metadata_in_url = {}
if git_url not in data: if git_url not in data:
nodes = set() nodes = set()
else: 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(): for x, desc in node_list_json.items():
nodes.add(x.strip()) nodes.add(x.strip())
metadata_in_url['title_aux'] = title
nodes = list(nodes) nodes = list(nodes)
nodes.sort() nodes.sort()
data[git_url] = nodes data[git_url] = (nodes, metadata_in_url)
json_path = f"extension-node-map.json" json_path = f"extension-node-map.json"
with open(json_path, "w") as file: with open(json_path, "w") as file:
@ -244,4 +260,4 @@ print("\n# Updating extensions\n")
updated_node_info = update_custom_nodes() updated_node_info = update_custom_nodes()
print("\n# 'extension-node-map.json' file is generated.\n") print("\n# 'extension-node-map.json' file is generated.\n")
gen_json(updated_node_info) gen_json(updated_node_info)

Loading…
Cancel
Save