Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8695cd3f1b | ||
|
|
cf865529ab | ||
|
|
3b9190a69b | ||
|
|
9a4eda3ef5 | ||
|
|
a2ecc11ebd | ||
|
|
7e9c97ecb4 | ||
|
|
3de160af25 | ||
|
|
3801a443bc | ||
|
|
bbdac97e49 | ||
|
|
50d51c70d0 | ||
|
|
55c9736a9b | ||
|
|
21729b2784 | ||
|
|
8d3cc39b72 | ||
|
|
abf1e82adb | ||
|
|
10d05031b1 | ||
|
|
7142b284ad | ||
|
|
11128ff85a | ||
|
|
a393793cfa |
@@ -29,6 +29,28 @@ jobs:
|
||||
with:
|
||||
submodules: "recursive"
|
||||
path: ${{ env.repo_name }}
|
||||
|
||||
# - name: 📝 Prepare file with paths to remove
|
||||
# run: |
|
||||
# find ${{ env.repo_name }} -type f -size +10M > .release_ignore
|
||||
# find ${{ env.repo_name }} -type d -empty >> .release_ignore
|
||||
# shell: bash
|
||||
|
||||
- name: 🗑️ Remove files and directories listed in .release_ignore
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f "${{ env.repo_name }}/.release_ignore" ]; then
|
||||
while IFS= read -r entry; do
|
||||
if [ -f "${{ env.repo_name }}/$entry" ]; then
|
||||
rm "${{ env.repo_name }}/$entry"
|
||||
elif [ -d "${{ env.repo_name }}/$entry" ]; then
|
||||
rm -r "${{ env.repo_name }}/$entry"
|
||||
fi
|
||||
done < "${{ env.repo_name }}/.release_ignore"
|
||||
else
|
||||
echo "No .release_ignore file found. Skipping removal of files and directories."
|
||||
fi
|
||||
|
||||
- name: 📦 Building custom comfy nodes
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
extern/frame_interpolation/moment.gif
|
||||
extern/frame_interpolation/photos
|
||||
extern/GFPGAN/inputs
|
||||
@@ -1,5 +1,11 @@
|
||||
## MTB Nodes
|
||||
|
||||
**Translated Readme (using DeepTranslate, PRs are welcome)**:
|
||||

|
||||
[日本語による説明](./README-JP.md)
|
||||

|
||||
[中文说明](./README-CN.md)
|
||||
|
||||
<a href="https://www.buymeacoffee.com/melmass" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 32px !important;width: 140px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||
|
||||
Welcome to the MTB Nodes project! This codebase is open for you to explore and utilize as you wish. Its primary purpose is to build proof-of-concepts (POCs) for implementation in [MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs). Many nodes in this project are inspired by existing community contributions or built-in functionalities.
|
||||
|
||||
+59
-8
@@ -1,3 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding:utf-8 -*-
|
||||
###
|
||||
# File: __init__.py
|
||||
# Project: comfy_mtb
|
||||
# Author: Mel Massadian
|
||||
# Copyright (c) 2023 Mel Massadian
|
||||
#
|
||||
###
|
||||
import os
|
||||
|
||||
os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
|
||||
@@ -5,6 +14,7 @@ os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
|
||||
import traceback
|
||||
from .log import log, blue_text, cyan_text, get_summary, get_label
|
||||
from .utils import here
|
||||
from .utils import comfy_dir
|
||||
import importlib
|
||||
import os
|
||||
import ast
|
||||
@@ -14,7 +24,7 @@ NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
NODE_CLASS_MAPPINGS_DEBUG = {}
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.1.1"
|
||||
|
||||
|
||||
def extract_nodes_from_source(filename):
|
||||
@@ -89,7 +99,7 @@ def load_nodes():
|
||||
|
||||
|
||||
# - REGISTER WEB EXTENSIONS
|
||||
web_extensions_root = utils.comfy_dir / "web" / "extensions"
|
||||
web_extensions_root = comfy_dir / "web" / "extensions"
|
||||
web_mtb = web_extensions_root / "mtb"
|
||||
|
||||
if web_mtb.exists():
|
||||
@@ -158,18 +168,35 @@ log.info(
|
||||
|
||||
# - ENDPOINT
|
||||
from server import PromptServer
|
||||
from .log import mklog, log
|
||||
from .log import log
|
||||
from aiohttp import web
|
||||
from importlib import reload
|
||||
import logging
|
||||
|
||||
endlog = mklog("endpoint")
|
||||
from .endpoint import endlog
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/mtb/status")
|
||||
async def get_full_library(request):
|
||||
files = []
|
||||
endlog.debug("Getting status")
|
||||
from . import endpoint
|
||||
|
||||
reload(endpoint)
|
||||
|
||||
endlog.debug("Getting node registration status")
|
||||
# Check if the request prefers HTML content
|
||||
if "text/html" in request.headers.get("Accept", ""):
|
||||
# # Return an HTML page
|
||||
html_response = endpoint.render_table(
|
||||
NODE_CLASS_MAPPINGS_DEBUG, title="Registered"
|
||||
)
|
||||
html_response += endpoint.render_table(
|
||||
{k: "-" for k in failed}, title="Failed to load"
|
||||
)
|
||||
|
||||
return web.Response(
|
||||
text=endpoint.render_base_template("MTB", html_response),
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"registered": NODE_CLASS_MAPPINGS_DEBUG,
|
||||
@@ -185,7 +212,7 @@ async def set_debug(request):
|
||||
if enabled:
|
||||
os.environ["MTB_DEBUG"] = "true"
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug("Debug mode set")
|
||||
log.debug("Debug mode set from API (/mtb/debug POST route)")
|
||||
|
||||
else:
|
||||
if "MTB_DEBUG" in os.environ:
|
||||
@@ -242,6 +269,30 @@ async def get_debug(request):
|
||||
return web.json_response({"enabled": enabled})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/mtb/actions")
|
||||
async def no_route(request):
|
||||
from . import endpoint
|
||||
|
||||
if "text/html" in request.headers.get("Accept", ""):
|
||||
html_response = f"""
|
||||
<h1>Actions has no get for now...</h1>
|
||||
"""
|
||||
return web.Response(
|
||||
text=endpoint.render_base_template("Actions", html_response),
|
||||
content_type="text/html",
|
||||
)
|
||||
return web.json_response({"message": "actions has no get for now"})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/mtb/actions")
|
||||
async def do_action(request):
|
||||
from . import endpoint
|
||||
|
||||
reload(endpoint)
|
||||
|
||||
return await endpoint.do_action(request)
|
||||
|
||||
|
||||
# - WAS Dictionary
|
||||
MANIFEST = {
|
||||
"name": "MTB Nodes", # The title that will be displayed on Node Class menu,. and Node Class view
|
||||
|
||||
+83
-1
@@ -1,4 +1,84 @@
|
||||
from .utils import here
|
||||
from aiohttp import web
|
||||
from .log import mklog
|
||||
import os
|
||||
|
||||
endlog = mklog("mtb endpoint")
|
||||
|
||||
#- ACTIONS
|
||||
|
||||
def ACTIONS_getStyles(style_name=None):
|
||||
from .nodes.conditions import StylesLoader
|
||||
|
||||
styles = StylesLoader.options
|
||||
match_list = ["name"]
|
||||
if styles:
|
||||
filtered_styles = {
|
||||
key: value
|
||||
for key, value in styles.items()
|
||||
if not key.startswith("__") and key not in match_list
|
||||
}
|
||||
if style_name:
|
||||
if style_name in filtered_styles:
|
||||
return filtered_styles[style_name]
|
||||
else:
|
||||
return {"error": "Style not found"}
|
||||
return filtered_styles
|
||||
return {"error": "No styles found"}
|
||||
|
||||
|
||||
async def do_action(request) -> web.Response:
|
||||
endlog.debug("Init action request")
|
||||
request_data = await request.json()
|
||||
name = request_data.get("name")
|
||||
args = request_data.get("args")
|
||||
|
||||
endlog.debug(f"Received action request: {name} {args}")
|
||||
|
||||
method_name = "ACTIONS_" + name
|
||||
method = globals().get(method_name)
|
||||
|
||||
if callable(method):
|
||||
result = method(args) if args else method()
|
||||
endlog.debug(f"Action result: {result}")
|
||||
return web.json_response({"result": result})
|
||||
|
||||
available_methods = [
|
||||
attr[len("ACTIONS_") :] for attr in globals() if attr.startswith("ACTIONS_")
|
||||
]
|
||||
|
||||
return web.json_response(
|
||||
{"error": "Invalid method name.", "available_methods": available_methods}
|
||||
)
|
||||
|
||||
|
||||
# - HTML UTILS
|
||||
def render_table(table_dict, sort=True, title=None):
|
||||
table_rows = ""
|
||||
table_dict = sorted(
|
||||
table_dict.items(), key=lambda item: item[0]
|
||||
) # Sort the dictionary by keys
|
||||
|
||||
for name, description in table_dict:
|
||||
table_rows += f"<tr><td>{name}</td><td>{description}</td></tr>"
|
||||
|
||||
html_response = f"""
|
||||
<div class="table-container">
|
||||
{"" if title is None else f"<h1>{title}</h1>"}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{table_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
return html_response
|
||||
|
||||
|
||||
def render_base_template(title, content):
|
||||
@@ -20,8 +100,10 @@ def render_base_template(title, content):
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="/">Back to Comfy</a>
|
||||
<div class="mtb_logo">
|
||||
<img src="https://repository-images.githubusercontent.com/649047066/a3eef9a7-20dd-4ef9-b839-884502d4e873" alt="Comfy MTB Logo" height="70" width="128">
|
||||
<span class="title">Comfy MTB</span>
|
||||
<span class="title">Comfy MTB</span></div>
|
||||
<a style="width:128px;text-align:center" href="https://www.github.com/melmass/comfy_mtb">
|
||||
{github_icon_svg}
|
||||
</a>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,932 @@
|
||||
{
|
||||
"last_node_id": 24,
|
||||
"last_link_id": 30,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 11,
|
||||
"type": "Get Batch From History (mtb)",
|
||||
"pos": [
|
||||
-828,
|
||||
522
|
||||
],
|
||||
"size": [
|
||||
235.1999969482422,
|
||||
118
|
||||
],
|
||||
"flags": {},
|
||||
"order": 12,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "passthrough_image",
|
||||
"type": "IMAGE",
|
||||
"link": 10
|
||||
},
|
||||
{
|
||||
"name": "enable",
|
||||
"type": "BOOL",
|
||||
"link": 9,
|
||||
"widget": {
|
||||
"name": "enable",
|
||||
"config": [
|
||||
"BOOL",
|
||||
{
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "i",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
26
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Get Batch From History (mtb)"
|
||||
},
|
||||
"widgets_values": [
|
||||
false,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"color": "#223",
|
||||
"bgcolor": "#335"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"type": "Note",
|
||||
"pos": [
|
||||
-827,
|
||||
406
|
||||
],
|
||||
"size": [
|
||||
229.35427033319013,
|
||||
69.73426380916959
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"properties": {
|
||||
"text": ""
|
||||
},
|
||||
"widgets_values": [
|
||||
"On first frame we get the init image, on all subsequent ones the feedback from the previous queue item"
|
||||
],
|
||||
"color": "#223",
|
||||
"bgcolor": "#335",
|
||||
"shape": 1
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-1388,
|
||||
321
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
10
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [
|
||||
-1679,
|
||||
817
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
8
|
||||
],
|
||||
"widget": {
|
||||
"name": "int",
|
||||
"config": [
|
||||
"INT",
|
||||
{
|
||||
"default": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "int",
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"increment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "Int To Bool (mtb)",
|
||||
"pos": [
|
||||
-1290,
|
||||
818
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
42.2801628112793
|
||||
],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "int",
|
||||
"type": "INT",
|
||||
"link": 8,
|
||||
"widget": {
|
||||
"name": "int",
|
||||
"config": [
|
||||
"INT",
|
||||
{
|
||||
"default": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "BOOL",
|
||||
"type": "BOOL",
|
||||
"links": [
|
||||
9
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Int To Bool (mtb)"
|
||||
},
|
||||
"widgets_values": [
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "Transform Image (mtb)",
|
||||
"pos": [
|
||||
-54,
|
||||
515
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
154
|
||||
],
|
||||
"flags": {},
|
||||
"order": 13,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 26
|
||||
},
|
||||
{
|
||||
"name": "angle",
|
||||
"type": "FLOAT",
|
||||
"link": 30,
|
||||
"widget": {
|
||||
"name": "angle",
|
||||
"config": [
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
24
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Transform Image (mtb)"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
5,
|
||||
1.02,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "Export To Prores (mtb)",
|
||||
"pos": [
|
||||
394,
|
||||
1803
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 17
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "VIDEO",
|
||||
"type": "VIDEO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Export To Prores (mtb)"
|
||||
},
|
||||
"widgets_values": [
|
||||
12,
|
||||
"export"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "Save Gif (mtb)",
|
||||
"pos": [
|
||||
392,
|
||||
1321
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
336
|
||||
],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Save Gif (mtb)"
|
||||
},
|
||||
"widgets_values": [
|
||||
12,
|
||||
1,
|
||||
true,
|
||||
"/view?filename=8b62123f3c.gif&subfolder=&type=output"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "Styles Loader (mtb)",
|
||||
"pos": [
|
||||
72,
|
||||
213
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
78
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "STRING",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
15
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Styles Loader (mtb)"
|
||||
},
|
||||
"widgets_values": [
|
||||
"❌Low Token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
64,
|
||||
68
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [
|
||||
4
|
||||
],
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [
|
||||
5
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
6
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"revAnimated_v122.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"type": "Fit Number (mtb)",
|
||||
"pos": [
|
||||
39,
|
||||
842
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
166
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "value",
|
||||
"type": "FLOAT",
|
||||
"link": 27
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "FLOAT",
|
||||
"type": "FLOAT",
|
||||
"links": [
|
||||
28
|
||||
],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Fit Number (mtb)"
|
||||
},
|
||||
"widgets_values": [
|
||||
true,
|
||||
0,
|
||||
1,
|
||||
0.9,
|
||||
0.2
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"type": "Fit Number (mtb)",
|
||||
"pos": [
|
||||
-342,
|
||||
621
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
166
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "value",
|
||||
"type": "FLOAT",
|
||||
"link": 29
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "FLOAT",
|
||||
"type": "FLOAT",
|
||||
"links": [
|
||||
30
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Fit Number (mtb)"
|
||||
},
|
||||
"widgets_values": [
|
||||
true,
|
||||
0.7,
|
||||
1,
|
||||
0,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "## i2i [cac944]",
|
||||
"pos": [
|
||||
567,
|
||||
65
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
230
|
||||
],
|
||||
"flags": {},
|
||||
"order": 14,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 4,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 5
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 6
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 24
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "STRING",
|
||||
"link": 15,
|
||||
"widget": {
|
||||
"name": "negative",
|
||||
"config": [
|
||||
"STRING",
|
||||
{
|
||||
"multiline": true,
|
||||
"placeholder": "negative",
|
||||
"default": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"slot_index": 4
|
||||
},
|
||||
{
|
||||
"name": "denoise",
|
||||
"type": "FLOAT",
|
||||
"link": 28,
|
||||
"widget": {
|
||||
"name": "denoise",
|
||||
"config": [
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.6,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01
|
||||
}
|
||||
]
|
||||
},
|
||||
"slot_index": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
25
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"title": "## i2i",
|
||||
"properties": {
|
||||
"Node name for S&R": "## i2i [cac944]"
|
||||
},
|
||||
"widgets_values": [
|
||||
"A blond girl in a pink dress, clear sky, 1958",
|
||||
"",
|
||||
0.2599999999999997
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1054,
|
||||
67
|
||||
],
|
||||
"size": [
|
||||
1110.1168110198978,
|
||||
630.0240418701173
|
||||
],
|
||||
"flags": {},
|
||||
"order": 15,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 25
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"ComfyUI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "Animation Builder (mtb)",
|
||||
"pos": [
|
||||
-1032,
|
||||
1274
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
306
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "frame",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "0-1 (scaled)",
|
||||
"type": "FLOAT",
|
||||
"links": [
|
||||
27,
|
||||
29
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "loop_ended",
|
||||
"type": "BOOL",
|
||||
"links": [
|
||||
16
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Animation Builder (mtb)"
|
||||
},
|
||||
"widgets_values": [
|
||||
30,
|
||||
1,
|
||||
1,
|
||||
90,
|
||||
1,
|
||||
"raw: 90\nframe: 0",
|
||||
"Done 😎!",
|
||||
"reset",
|
||||
"queue"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "Get Batch From History (mtb)",
|
||||
"pos": [
|
||||
-369,
|
||||
1511
|
||||
],
|
||||
"size": [
|
||||
235.1999969482422,
|
||||
118
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "passthrough_image",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "enable",
|
||||
"type": "BOOL",
|
||||
"link": 16,
|
||||
"widget": {
|
||||
"name": "enable",
|
||||
"config": [
|
||||
"BOOL",
|
||||
{
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "i",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
17,
|
||||
18
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Get Batch From History (mtb)"
|
||||
},
|
||||
"widgets_values": [
|
||||
false,
|
||||
30,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
4,
|
||||
9,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
5,
|
||||
9,
|
||||
1,
|
||||
8,
|
||||
1,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
6,
|
||||
9,
|
||||
2,
|
||||
8,
|
||||
2,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
8,
|
||||
13,
|
||||
0,
|
||||
12,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
9,
|
||||
12,
|
||||
0,
|
||||
11,
|
||||
1,
|
||||
"BOOL"
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
0,
|
||||
11,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
15,
|
||||
16,
|
||||
1,
|
||||
8,
|
||||
4,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
16,
|
||||
17,
|
||||
3,
|
||||
18,
|
||||
1,
|
||||
"BOOL"
|
||||
],
|
||||
[
|
||||
17,
|
||||
18,
|
||||
0,
|
||||
20,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
18,
|
||||
18,
|
||||
0,
|
||||
19,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
24,
|
||||
14,
|
||||
0,
|
||||
8,
|
||||
3,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
25,
|
||||
8,
|
||||
0,
|
||||
15,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
26,
|
||||
11,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
27,
|
||||
17,
|
||||
1,
|
||||
22,
|
||||
0,
|
||||
"FLOAT"
|
||||
],
|
||||
[
|
||||
28,
|
||||
22,
|
||||
0,
|
||||
8,
|
||||
5,
|
||||
"FLOAT"
|
||||
],
|
||||
[
|
||||
29,
|
||||
17,
|
||||
1,
|
||||
23,
|
||||
0,
|
||||
"FLOAT"
|
||||
],
|
||||
[
|
||||
30,
|
||||
23,
|
||||
0,
|
||||
14,
|
||||
1,
|
||||
"FLOAT"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"title": "Init Image",
|
||||
"bounding": [
|
||||
-1730,
|
||||
215,
|
||||
716,
|
||||
713
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"title": "Video Output",
|
||||
"bounding": [
|
||||
-426,
|
||||
1176,
|
||||
1223,
|
||||
787
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"title": "Animation",
|
||||
"bounding": [
|
||||
-473,
|
||||
363,
|
||||
900,
|
||||
718
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"locked": false
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
+82
-1
@@ -6,6 +6,87 @@ html {
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
a {
|
||||
color: whitesmoke;
|
||||
|
||||
}
|
||||
|
||||
.table-container {
|
||||
width: 70%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgb(45, 45, 45);
|
||||
/* Light gray background for header row */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: rgb(45, 45, 45);
|
||||
/* Alternate row background color */
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #797979;
|
||||
/* Highlight color on hover */
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
/* Applies to the second column (Description) */
|
||||
width: 80%;
|
||||
/* Adjust the width as needed */
|
||||
word-wrap: break-word;
|
||||
/* Allow long words to be broken and wrapped to the next line */
|
||||
}
|
||||
|
||||
.mtb_logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Styling for WebKit-based browsers (Chrome, Edge) */
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
/* Set the width of the scrollbar */
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background-color: #797979;
|
||||
/* Color of the scrollbar thumb */
|
||||
}
|
||||
|
||||
/* Styling for Firefox */
|
||||
.table-container {
|
||||
scrollbar-width: thin;
|
||||
/* Set the width of the scrollbar */
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background-color: #797979;
|
||||
/* Color of the scrollbar thumb */
|
||||
}
|
||||
|
||||
/* Optionally, you can also style the scrollbar track (background) */
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -18,7 +99,7 @@ body {
|
||||
.title {
|
||||
font-size: 2.5em;
|
||||
font-weight: 700;
|
||||
margin: 1em;
|
||||
|
||||
}
|
||||
|
||||
header {
|
||||
|
||||
@@ -4,9 +4,6 @@ from ..log import log
|
||||
class AnimationBuilder:
|
||||
"""Convenient way to manage basic animation maths at the core of many of my workflows"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
|
||||
+41
-36
@@ -1,5 +1,5 @@
|
||||
from ..utils import pil2tensor
|
||||
from ..utils import here
|
||||
from ..utils import here, comfy_dir
|
||||
from ..log import log
|
||||
import folder_paths
|
||||
from pathlib import Path
|
||||
@@ -10,9 +10,6 @@ import csv
|
||||
class SmartStep:
|
||||
"""Utils to control the steps start/stop of the KAdvancedSampler in percentage"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -62,30 +59,28 @@ class StylesLoader:
|
||||
|
||||
options = {}
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
input_dir = Path(folder_paths.base_path) / "styles"
|
||||
if not input_dir.exists():
|
||||
install_default_styles()
|
||||
if not cls.options:
|
||||
input_dir = Path(folder_paths.base_path) / "styles"
|
||||
if not input_dir.exists():
|
||||
install_default_styles()
|
||||
|
||||
if not (files := [f for f in input_dir.iterdir() if f.suffix == ".csv"]):
|
||||
log.warn(
|
||||
"No styles found in the styles folder, place at least one csv file in the styles folder at the root of ComfyUI (for instance ComfyUI/styles/mystyle.csv)"
|
||||
)
|
||||
|
||||
for file in files:
|
||||
with open(file, "r", encoding="utf8") as f:
|
||||
parsed = csv.reader(f)
|
||||
for row in parsed:
|
||||
log.debug(f"Adding style {row[0]}")
|
||||
cls.options[row[0]] = (row[1], row[2])
|
||||
|
||||
else:
|
||||
log.debug(f"Using cached styles (count: {len(cls.options)})")
|
||||
|
||||
if not (files := [f for f in input_dir.iterdir() if f.suffix == ".csv"]):
|
||||
log.error(
|
||||
"No styles found in the styles folder, place at least one csv file in the styles folder"
|
||||
)
|
||||
return {
|
||||
"required": {
|
||||
"style_name": (["error"],),
|
||||
}
|
||||
}
|
||||
for file in files:
|
||||
with open(file, "r", encoding="utf8") as f:
|
||||
parsed = csv.reader(f)
|
||||
for row in parsed:
|
||||
log.debug(f"Adding style {row[0]}")
|
||||
cls.options[row[0]] = (row[1], row[2])
|
||||
return {
|
||||
"required": {
|
||||
"style_name": (list(cls.options.keys()),),
|
||||
@@ -112,24 +107,34 @@ class TextToImage:
|
||||
fonts = {}
|
||||
|
||||
def __init__(self):
|
||||
# - This is executed when the graph is executed, we could conditionaly reload fonts there
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
fonts = list(Path(folder_paths.base_path).glob("**/*.ttf"))
|
||||
def CACHE_FONTS(cls):
|
||||
font_extensions = ["*.ttf", "*.otf", "*.woff", "*.woff2", "*.eot"]
|
||||
fonts = []
|
||||
|
||||
for extension in font_extensions:
|
||||
fonts.extend(comfy_dir.glob(f"**/{extension}"))
|
||||
|
||||
if not fonts:
|
||||
log.error(
|
||||
"No fonts found in the fonts folder, place at least one ttf file in the fonts folder"
|
||||
log.warn(
|
||||
"> No fonts found in the comfy folder, place at least one font file somewhere in ComfyUI's hierarchy"
|
||||
)
|
||||
return {
|
||||
"required": {
|
||||
"font": (["error"],),
|
||||
}
|
||||
}
|
||||
else:
|
||||
log.debug(f"> Found {len(fonts)} fonts")
|
||||
|
||||
for font in fonts:
|
||||
log.debug(f"Adding font {font}")
|
||||
cls.fonts[font.stem] = font.as_posix()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
if not cls.fonts:
|
||||
cls.CACHE_FONTS()
|
||||
else:
|
||||
log.debug(f"Using cached fonts (count: {len(cls.fonts)})")
|
||||
return {
|
||||
"required": {
|
||||
"text": (
|
||||
@@ -143,11 +148,11 @@ class TextToImage:
|
||||
),
|
||||
"font_size": (
|
||||
"INT",
|
||||
{"default": 12, "min": 1, "max": 100, "step": 1},
|
||||
{"default": 12, "min": 1, "max": 2500, "step": 1},
|
||||
),
|
||||
"width": (
|
||||
"INT",
|
||||
{"default": 512, "min": 1, "max": 1000, "step": 1},
|
||||
{"default": 512, "min": 1, "max": 8096, "step": 1},
|
||||
),
|
||||
"height": (
|
||||
"INT",
|
||||
|
||||
@@ -9,9 +9,6 @@ from ..log import log
|
||||
class Bbox:
|
||||
"""The bounding box (BBOX) custom type used by other nodes"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -42,9 +39,6 @@ class Bbox:
|
||||
class BboxFromMask:
|
||||
"""From a mask extract the bounding box"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -116,9 +110,6 @@ class Crop:
|
||||
The BBOX input takes precedence over the tuple input
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -205,9 +196,6 @@ class Uncrop:
|
||||
The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type
|
||||
The BBOX input takes precedence over the tuple input"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
|
||||
+65
-1
@@ -2,6 +2,9 @@ from ..utils import tensor2pil
|
||||
from ..log import log
|
||||
import io, base64
|
||||
import torch
|
||||
import folder_paths
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Debug:
|
||||
@@ -54,4 +57,65 @@ class Debug:
|
||||
return output
|
||||
|
||||
|
||||
__nodes__ = [Debug]
|
||||
class SaveTensors:
|
||||
"""Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy"""
|
||||
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "mtb/debug"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"filename_prefix": ("STRING", {"default": "ComfyPickle"}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
"latent": ("LATENT",),
|
||||
},
|
||||
}
|
||||
|
||||
FUNCTION = "save"
|
||||
OUTPUT_NODE = True
|
||||
RETURN_TYPES = ()
|
||||
CATEGORY = "mtb/debug"
|
||||
|
||||
def save(
|
||||
self,
|
||||
filename_prefix,
|
||||
image: Optional[torch.Tensor] = None,
|
||||
mask: Optional[torch.Tensor] = None,
|
||||
latent: Optional[torch.Tensor] = None,
|
||||
):
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
counter,
|
||||
subfolder,
|
||||
filename_prefix,
|
||||
) = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
|
||||
full_output_folder = Path(full_output_folder)
|
||||
if image is not None:
|
||||
image_file = f"{filename}_image_{counter:05}.pt"
|
||||
torch.save(image, full_output_folder / image_file)
|
||||
# np.save(full_output_folder/ image_file, image.cpu().numpy())
|
||||
|
||||
if mask is not None:
|
||||
mask_file = f"{filename}_mask_{counter:05}.pt"
|
||||
torch.save(mask, full_output_folder / mask_file)
|
||||
# np.save(full_output_folder/ mask_file, mask.cpu().numpy())
|
||||
|
||||
if latent is not None:
|
||||
# for latent we must use pickle
|
||||
latent_file = f"{filename}_latent_{counter:05}.pt"
|
||||
torch.save(latent, full_output_folder / latent_file)
|
||||
# pickle.dump(latent, open(full_output_folder/ latent_file, "wb"))
|
||||
|
||||
# np.save(full_output_folder/ latent_file, latent[""].cpu().numpy())
|
||||
|
||||
return f"{filename_prefix}_{counter:05}"
|
||||
|
||||
|
||||
__nodes__ = [Debug, SaveTensors]
|
||||
|
||||
+3
-8
@@ -241,9 +241,6 @@ def normals_to_height(normals_img, seamless, progress_callback):
|
||||
class DeepBump:
|
||||
"""Normal & height maps generation from single pictures"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -264,7 +261,7 @@ class DeepBump:
|
||||
"LARGEST",
|
||||
],
|
||||
),
|
||||
"normals_to_height_seamless": (["TRUE", "FALSE"],),
|
||||
"normals_to_height_seamless": ("BOOL", {"default": False}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -279,7 +276,7 @@ class DeepBump:
|
||||
mode="Color to Normals",
|
||||
color_to_normals_overlap="SMALL",
|
||||
normals_to_curvature_blur_radius="SMALL",
|
||||
normals_to_height_seamless="TRUE",
|
||||
normals_to_height_seamless=True,
|
||||
):
|
||||
image = utils_inference.tensor2pil(image)
|
||||
|
||||
@@ -295,9 +292,7 @@ class DeepBump:
|
||||
in_img, normals_to_curvature_blur_radius, None
|
||||
)
|
||||
if mode == "Normals to Height":
|
||||
out_img = normals_to_height(
|
||||
in_img, normals_to_height_seamless == "TRUE", None
|
||||
)
|
||||
out_img = normals_to_height(in_img, normals_to_height_seamless, None)
|
||||
|
||||
out_img = (np.transpose(out_img, (1, 2, 0)) * 255).astype(np.uint8)
|
||||
|
||||
|
||||
+6
-10
@@ -136,12 +136,12 @@ class RestoreFace:
|
||||
"image": ("IMAGE",),
|
||||
"model": ("FACEENHANCE_MODEL",),
|
||||
# Input are aligned faces
|
||||
"aligned": (["true", "false"], {"default": "false"}),
|
||||
"aligned": ("BOOL", {"default": False}),
|
||||
# Only restore the center face
|
||||
"only_center_face": (["true", "false"], {"default": "false"}),
|
||||
"only_center_face": ("BOOL", {"default": False}),
|
||||
# Adjustable weights
|
||||
"weight": ("FLOAT", {"default": 0.5}),
|
||||
"save_tmp_steps": (["true", "false"], {"default": "true"}),
|
||||
"save_tmp_steps": ("BOOL", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,15 +183,11 @@ class RestoreFace:
|
||||
self,
|
||||
image: torch.Tensor,
|
||||
model: GFPGANer,
|
||||
aligned="false",
|
||||
only_center_face="false",
|
||||
aligned=False,
|
||||
only_center_face=False,
|
||||
weight=0.5,
|
||||
save_tmp_steps="true",
|
||||
save_tmp_steps=True,
|
||||
) -> Tuple[torch.Tensor]:
|
||||
save_tmp_steps = save_tmp_steps == "true"
|
||||
aligned = aligned == "true"
|
||||
only_center_face = only_center_face == "true"
|
||||
|
||||
out = [
|
||||
self.do_restore(
|
||||
image[i], model, aligned, only_center_face, weight, save_tmp_steps
|
||||
|
||||
+3
-2
@@ -82,8 +82,9 @@ class FaceSwap:
|
||||
"reference": ("IMAGE",),
|
||||
"faces_index": ("STRING", {"default": "0"}),
|
||||
"faceswap_model": ("FACESWAP_MODEL", {"default": "None"}),
|
||||
"debug": ("BOOL", {"default": False}),
|
||||
},
|
||||
"optional": {"debug": (["true", "false"], {"default": "false"})},
|
||||
"optional": {},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
@@ -96,7 +97,7 @@ class FaceSwap:
|
||||
reference: torch.Tensor,
|
||||
faces_index: str,
|
||||
faceswap_model,
|
||||
debug="false",
|
||||
debug=False,
|
||||
):
|
||||
def do_swap(img):
|
||||
model_management.throw_exception_if_processing_interrupted()
|
||||
|
||||
+56
-6
@@ -1,6 +1,7 @@
|
||||
import qrcode
|
||||
from ..utils import pil2tensor
|
||||
from PIL import Image
|
||||
from ..log import log
|
||||
|
||||
# class MtbExamples:
|
||||
# """MTB Example Images"""
|
||||
@@ -50,12 +51,60 @@ from PIL import Image
|
||||
# return m.digest().hex()
|
||||
|
||||
|
||||
class UnsplashImage:
|
||||
"""Unsplash Image given a keyword and a size"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"width": ("INT", {"default": 512, "max": 8096, "min": 0, "step": 1}),
|
||||
"height": ("INT", {"default": 512, "max": 8096, "min": 0, "step": 1}),
|
||||
"random_seed": ("INT", {"default": 0, "max": 1e5, "min": 0, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"keyword": ("STRING", {"default": "nature"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "do_unsplash_image"
|
||||
CATEGORY = "mtb/generate"
|
||||
|
||||
def do_unsplash_image(self, width, height, random_seed, keyword=None):
|
||||
import requests
|
||||
import io
|
||||
|
||||
base_url = "https://source.unsplash.com/random/"
|
||||
|
||||
if width and height:
|
||||
base_url += f"/{width}x{height}"
|
||||
|
||||
if keyword:
|
||||
keyword = keyword.replace(" ", "%20")
|
||||
base_url += f"?{keyword}&{random_seed}"
|
||||
else:
|
||||
base_url += f"?&{random_seed}"
|
||||
try:
|
||||
log.debug(f"Getting unsplash image from {base_url}")
|
||||
response = requests.get(base_url)
|
||||
response.raise_for_status()
|
||||
|
||||
image = Image.open(io.BytesIO(response.content))
|
||||
return (
|
||||
pil2tensor(
|
||||
image,
|
||||
),
|
||||
)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print("Error retrieving image:", e)
|
||||
return (None,)
|
||||
|
||||
|
||||
class QrCode:
|
||||
"""Basic QR Code generator"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -72,7 +121,7 @@ class QrCode:
|
||||
"error_correct": (("L", "M", "Q", "H"), {"default": "L"}),
|
||||
"box_size": ("INT", {"default": 10, "max": 8096, "min": 0, "step": 1}),
|
||||
"border": ("INT", {"default": 4, "max": 8096, "min": 0, "step": 1}),
|
||||
"invert": (("True", "False"), {"default": "False"}),
|
||||
"invert": (("BOOL",), {"default": False}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +148,8 @@ class QrCode:
|
||||
qr.add_data(url)
|
||||
qr.make(fit=True)
|
||||
|
||||
back_color = (255, 255, 255) if invert == "True" else (0, 0, 0)
|
||||
fill_color = (0, 0, 0) if invert == "True" else (255, 255, 255)
|
||||
back_color = (255, 255, 255) if invert else (0, 0, 0)
|
||||
fill_color = (0, 0, 0) if invert else (255, 255, 255)
|
||||
|
||||
code = img = qr.make_image(back_color=back_color, fill_color=fill_color)
|
||||
|
||||
@@ -112,5 +161,6 @@ class QrCode:
|
||||
|
||||
__nodes__ = [
|
||||
QrCode,
|
||||
UnsplashImage
|
||||
# MtbExamples,
|
||||
]
|
||||
|
||||
@@ -43,6 +43,7 @@ class GetBatchFromHistory:
|
||||
"count": ("INT", {"default": 1, "min": 0}),
|
||||
"offset": ("INT", {"default": 0, "min": -1e9, "max": 1e9}),
|
||||
},
|
||||
"optional": {"passthrough_image": ("IMAGE",)},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
@@ -55,8 +56,11 @@ class GetBatchFromHistory:
|
||||
enable=True,
|
||||
count=0,
|
||||
offset=0,
|
||||
passthrough_image=None,
|
||||
):
|
||||
if not enable or count == 0:
|
||||
if passthrough_image is not None:
|
||||
return (passthrough_image,)
|
||||
log.debug("Load from history is disabled for this iteration")
|
||||
return (torch.zeros(0),)
|
||||
frames = []
|
||||
@@ -145,9 +149,6 @@ class LoadFilmModel:
|
||||
class FilmInterpolation:
|
||||
"""Google Research FILM frame interpolation for large motion"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -210,9 +211,6 @@ class FilmInterpolation:
|
||||
class ConcatImages:
|
||||
"""Add images to batch"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "concat_images"
|
||||
CATEGORY = "mtb/image"
|
||||
|
||||
+11
-32
@@ -17,18 +17,15 @@ import os
|
||||
import comfy.model_management as model_management
|
||||
|
||||
|
||||
try:
|
||||
from cv2.ximgproc import guidedFilter
|
||||
except ImportError:
|
||||
log.warning("cv2.ximgproc.guidedFilter not found, use opencv-contrib-python")
|
||||
# try:
|
||||
# from cv2.ximgproc import guidedFilter
|
||||
# except ImportError:
|
||||
# log.warning("cv2.ximgproc.guidedFilter not found, use opencv-contrib-python")
|
||||
|
||||
|
||||
class ColorCorrect:
|
||||
"""Various color correction methods"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -187,9 +184,6 @@ class ColorCorrect:
|
||||
class ImageCompare:
|
||||
"""Compare two images and return a difference image"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -252,9 +246,6 @@ class LoadImageFromUrl:
|
||||
class Blur:
|
||||
"""Blur an image using a Gaussian filter."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -312,9 +303,6 @@ class Blur:
|
||||
class MaskToImage:
|
||||
"""Converts a mask (alpha) to an RGB image with a color and background"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -383,16 +371,13 @@ class ColoredImage:
|
||||
class ImagePremultiply:
|
||||
"""Premultiply image with mask"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
"invert": (["True", "False"], {"default": "False"}),
|
||||
"invert": ("BOOL", {"default": False}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,8 +386,6 @@ class ImagePremultiply:
|
||||
FUNCTION = "premultiply"
|
||||
|
||||
def premultiply(self, image, mask, invert):
|
||||
invert = invert == "True"
|
||||
|
||||
images = tensor2pil(image)
|
||||
if invert:
|
||||
masks = tensor2pil(mask) # .convert("L")
|
||||
@@ -433,9 +416,6 @@ class ImagePremultiply:
|
||||
class ImageResizeFactor:
|
||||
"""Extracted mostly from WAS Node Suite, with a few edits (most notably multiple image support) and less features."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -445,7 +425,7 @@ class ImageResizeFactor:
|
||||
"FLOAT",
|
||||
{"default": 2, "min": 0.01, "max": 16.0, "step": 0.01},
|
||||
),
|
||||
"supersample": (["true", "false"], {"default": "true"}),
|
||||
"supersample": ("BOOL", {"default": True}),
|
||||
"resampling": (
|
||||
["lanczos", "nearest", "bilinear", "bicubic"],
|
||||
{"default": "lanczos"},
|
||||
@@ -510,7 +490,7 @@ class ImageResizeFactor:
|
||||
resample_filters = {"nearest": 0, "bilinear": 2, "bicubic": 3, "lanczos": 1}
|
||||
|
||||
# Apply supersample
|
||||
if supersample == "true":
|
||||
if supersample:
|
||||
super_size = (new_width * 8, new_height * 8)
|
||||
log.debug(f"Applying supersample: {super_size}")
|
||||
img = img.resize(
|
||||
@@ -529,12 +509,12 @@ class ImageResizeFactor:
|
||||
self,
|
||||
image: torch.Tensor,
|
||||
factor: float,
|
||||
supersample: str,
|
||||
supersample: bool,
|
||||
resampling: str,
|
||||
mask=None,
|
||||
):
|
||||
log.debug(f"Resizing image with factor {factor} and resampling {resampling}")
|
||||
supersample = supersample == "true"
|
||||
|
||||
batch_count = image.size(0)
|
||||
log.debug(f"Batch count: {batch_count}")
|
||||
if batch_count == 1:
|
||||
@@ -566,7 +546,7 @@ class SaveImageGrid:
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||
"save_intermediate": (["true", "false"], {"default": "false"}),
|
||||
"save_intermediate": ("BOOL", {"default": False}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
@@ -607,11 +587,10 @@ class SaveImageGrid:
|
||||
self,
|
||||
images,
|
||||
filename_prefix="Grid",
|
||||
save_intermediate="false",
|
||||
save_intermediate=False,
|
||||
prompt=None,
|
||||
extra_pnginfo=None,
|
||||
):
|
||||
save_intermediate = save_intermediate == "true"
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
|
||||
@@ -12,9 +12,6 @@ import numpy as np
|
||||
class ExportToProres:
|
||||
"""Export to ProRes 4444 (Experimental)"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import torch
|
||||
|
||||
|
||||
class LatentLerp:
|
||||
"""Linear interpolation (blend) between two latent vectors"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -28,6 +27,7 @@ class LatentLerp:
|
||||
|
||||
return (a,)
|
||||
|
||||
|
||||
__nodes__ = [
|
||||
LatentLerp,
|
||||
]
|
||||
]
|
||||
|
||||
+6
-9
@@ -7,17 +7,14 @@ import comfy.utils
|
||||
class ImageRemoveBackgroundRembg:
|
||||
"""Removes the background from the input using Rembg."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"alpha_matting": (
|
||||
["True", "False"],
|
||||
{"default": "False"},
|
||||
"BOOL",
|
||||
{"default": False},
|
||||
),
|
||||
"alpha_matting_foreground_threshold": (
|
||||
"INT",
|
||||
@@ -32,8 +29,8 @@ class ImageRemoveBackgroundRembg:
|
||||
{"default": 10, "min": 0, "max": 255},
|
||||
),
|
||||
"post_process_mask": (
|
||||
["True", "False"],
|
||||
{"default": "False"},
|
||||
"BOOL",
|
||||
{"default": False},
|
||||
),
|
||||
"bgcolor": (
|
||||
"COLOR",
|
||||
@@ -76,13 +73,13 @@ class ImageRemoveBackgroundRembg:
|
||||
for img in images:
|
||||
img_rm = remove(
|
||||
data=img,
|
||||
alpha_matting=alpha_matting == "True",
|
||||
alpha_matting=alpha_matting,
|
||||
alpha_matting_foreground_threshold=alpha_matting_foreground_threshold,
|
||||
alpha_matting_background_threshold=alpha_matting_background_threshold,
|
||||
alpha_matting_erode_size=alpha_matting_erode_size,
|
||||
session=None,
|
||||
only_mask=False,
|
||||
post_process_mask=post_process_mask == "True",
|
||||
post_process_mask=post_process_mask,
|
||||
bgcolor=None,
|
||||
)
|
||||
|
||||
|
||||
+2
-10
@@ -25,9 +25,6 @@ class IntToBool:
|
||||
class IntToNumber:
|
||||
"""Node addon for the WAS Suite. Converts a "comfy" INT to a NUMBER."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -56,9 +53,6 @@ class IntToNumber:
|
||||
class FloatToNumber:
|
||||
"""Node addon for the WAS Suite. Converts a "comfy" FLOAT to a NUMBER."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -83,13 +77,11 @@ class FloatToNumber:
|
||||
def float_to_number(self, float):
|
||||
return (float,)
|
||||
|
||||
|
||||
|
||||
return (int,)
|
||||
|
||||
|
||||
__nodes__ = [
|
||||
FloatToNumber,
|
||||
IntToBool,
|
||||
IntToNumber,
|
||||
|
||||
]
|
||||
]
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import torch
|
||||
import torchvision.transforms.functional as F
|
||||
|
||||
|
||||
class TransformImage:
|
||||
"""Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy
|
||||
|
||||
|
||||
it return a tensor representing the transformed images with the same shape as the input tensor
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"x": ("FLOAT", {"default": 0}),
|
||||
"y": ("FLOAT", {"default": 0}),
|
||||
"zoom": ("FLOAT", {"default": 1.0, "min": 0.001}),
|
||||
"angle": ("FLOAT", {"default": 0}),
|
||||
"shear": ("FLOAT", {"default": 0}),
|
||||
},
|
||||
}
|
||||
|
||||
FUNCTION = "transform"
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
CATEGORY = "mtb/transform"
|
||||
|
||||
def transform(
|
||||
self,
|
||||
image: torch.Tensor,
|
||||
x: float,
|
||||
y: float,
|
||||
zoom: float,
|
||||
angle: int,
|
||||
shear,
|
||||
):
|
||||
if image.size(0) == 0:
|
||||
return (torch.zeros(0),)
|
||||
transformed_images = []
|
||||
for img in image:
|
||||
img = img.transpose(0, 2)
|
||||
|
||||
transformed_image = F.affine(
|
||||
img, angle=angle, scale=zoom, translate=[int(y), int(x)], shear=shear
|
||||
)
|
||||
|
||||
transformed_image = transformed_image.transpose(2, 0)
|
||||
transformed_images.append(transformed_image.unsqueeze(0))
|
||||
|
||||
return (torch.cat(transformed_images, dim=0),)
|
||||
|
||||
|
||||
__nodes__ = [TransformImage]
|
||||
+241
-205
@@ -1,282 +1,318 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
/**
|
||||
* File: comfy_shared.js
|
||||
* Project: comfy_mtb
|
||||
* Author: Mel Massadian
|
||||
*
|
||||
* Copyright (c) 2023 Mel Massadian
|
||||
*
|
||||
*/
|
||||
|
||||
import { app } from '/scripts/app.js'
|
||||
|
||||
export const log = (...args) => {
|
||||
if (window.MTB_DEBUG) {
|
||||
console.debug(...args);
|
||||
}
|
||||
|
||||
if (window.MTB?.DEBUG) {
|
||||
console.debug(...args)
|
||||
}
|
||||
}
|
||||
|
||||
//- WIDGET UTILS
|
||||
export const CONVERTED_TYPE = "converted-widget";
|
||||
export const CONVERTED_TYPE = 'converted-widget'
|
||||
|
||||
export function offsetDOMWidget(widget, ctx, node, widgetWidth, widgetY, height) {
|
||||
const margin = 10;
|
||||
const elRect = ctx.canvas.getBoundingClientRect();
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(margin, margin + widgetY);
|
||||
export function offsetDOMWidget(
|
||||
widget,
|
||||
ctx,
|
||||
node,
|
||||
widgetWidth,
|
||||
widgetY,
|
||||
height
|
||||
) {
|
||||
const margin = 10
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(
|
||||
elRect.width / ctx.canvas.width,
|
||||
elRect.height / ctx.canvas.height
|
||||
)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(margin, margin + widgetY)
|
||||
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
||||
Object.assign(widget.inputEl.style, {
|
||||
transformOrigin: "0 0",
|
||||
transform: scale,
|
||||
left: `${transform.a + transform.e}px`,
|
||||
top: `${transform.d + transform.f}px`,
|
||||
width: `${widgetWidth - (margin * 2)}px`,
|
||||
// height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`,
|
||||
height: `${(height || widget.parent?.inputHeight || 32) - (margin * 2)}px`,
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
||||
Object.assign(widget.inputEl.style, {
|
||||
transformOrigin: '0 0',
|
||||
transform: scale,
|
||||
left: `${transform.a + transform.e}px`,
|
||||
top: `${transform.d + transform.f}px`,
|
||||
width: `${widgetWidth - margin * 2}px`,
|
||||
// height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`,
|
||||
height: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`,
|
||||
|
||||
position: "absolute",
|
||||
background: (!node.color) ? '' : node.color,
|
||||
color: (!node.color) ? '' : 'white',
|
||||
zIndex: app.graph._nodes.indexOf(node),
|
||||
})
|
||||
position: 'absolute',
|
||||
background: !node.color ? '' : node.color,
|
||||
color: !node.color ? '' : 'white',
|
||||
zIndex: app.graph._nodes.indexOf(node),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the type and link type from a widget config object.
|
||||
* @param {*} config
|
||||
* @returns
|
||||
* @param {*} config
|
||||
* @returns
|
||||
*/
|
||||
export function getWidgetType(config) {
|
||||
// Special handling for COMBO so we restrict links based on the entries
|
||||
let type = config[0];
|
||||
let linkType = type;
|
||||
if (type instanceof Array) {
|
||||
type = "COMBO";
|
||||
linkType = linkType.join(",");
|
||||
}
|
||||
return { type, linkType };
|
||||
// Special handling for COMBO so we restrict links based on the entries
|
||||
let type = config[0]
|
||||
let linkType = type
|
||||
if (type instanceof Array) {
|
||||
type = 'COMBO'
|
||||
linkType = linkType.join(',')
|
||||
}
|
||||
return { type, linkType }
|
||||
}
|
||||
|
||||
export const dynamic_connection = (node, index, connected, connectionPrefix = "input_", connectionType = "PSDLAYER") => {
|
||||
|
||||
// remove all non connected inputs
|
||||
if (!connected && node.inputs.length > 1) {
|
||||
log(`Removing input ${index} (${node.inputs[index].name})`)
|
||||
if (node.widgets) {
|
||||
const w = node.widgets.find((w) => w.name === node.inputs[index].name);
|
||||
if (w) {
|
||||
w.onRemove?.();
|
||||
node.widgets.length = node.widgets.length - 1
|
||||
}
|
||||
}
|
||||
node.removeInput(index)
|
||||
|
||||
// make inputs sequential again
|
||||
for (let i = 0; i < node.inputs.length; i++) {
|
||||
node.inputs[i].label = `${connectionPrefix}${i + 1}`
|
||||
}
|
||||
export const dynamic_connection = (
|
||||
node,
|
||||
index,
|
||||
connected,
|
||||
connectionPrefix = 'input_',
|
||||
connectionType = 'PSDLAYER'
|
||||
) => {
|
||||
// remove all non connected inputs
|
||||
if (!connected && node.inputs.length > 1) {
|
||||
log(`Removing input ${index} (${node.inputs[index].name})`)
|
||||
if (node.widgets) {
|
||||
const w = node.widgets.find((w) => w.name === node.inputs[index].name)
|
||||
if (w) {
|
||||
w.onRemoved?.()
|
||||
node.widgets.length = node.widgets.length - 1
|
||||
}
|
||||
}
|
||||
node.removeInput(index)
|
||||
|
||||
// add an extra input
|
||||
if (node.inputs[node.inputs.length - 1].link != undefined) {
|
||||
log(`Adding input ${node.inputs.length + 1} (${connectionPrefix}${node.inputs.length + 1})`)
|
||||
|
||||
node.addInput(`${connectionPrefix}${node.inputs.length + 1}`, connectionType)
|
||||
// make inputs sequential again
|
||||
for (let i = 0; i < node.inputs.length; i++) {
|
||||
node.inputs[i].label = `${connectionPrefix}${i + 1}`
|
||||
}
|
||||
}
|
||||
|
||||
// add an extra input
|
||||
if (node.inputs[node.inputs.length - 1].link != undefined) {
|
||||
log(
|
||||
`Adding input ${node.inputs.length + 1} (${connectionPrefix}${
|
||||
node.inputs.length + 1
|
||||
})`
|
||||
)
|
||||
|
||||
node.addInput(
|
||||
`${connectionPrefix}${node.inputs.length + 1}`,
|
||||
connectionType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends a callback to the extra menu options of a given node type.
|
||||
* @param {*} nodeType
|
||||
* @param {*} cb
|
||||
* @param {*} nodeType
|
||||
* @param {*} cb
|
||||
*/
|
||||
export function addMenuHandler(nodeType, cb) {
|
||||
const getOpts = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function () {
|
||||
const r = getOpts.apply(this, arguments);
|
||||
cb.apply(this, arguments);
|
||||
return r;
|
||||
};
|
||||
const getOpts = nodeType.prototype.getExtraMenuOptions
|
||||
nodeType.prototype.getExtraMenuOptions = function () {
|
||||
const r = getOpts.apply(this, arguments)
|
||||
cb.apply(this, arguments)
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
export function hideWidget(node, widget, suffix = "") {
|
||||
widget.origType = widget.type;
|
||||
widget.hidden = true
|
||||
widget.origComputeSize = widget.computeSize;
|
||||
widget.origSerializeValue = widget.serializeValue;
|
||||
widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
|
||||
widget.type = CONVERTED_TYPE + suffix;
|
||||
widget.serializeValue = () => {
|
||||
// Prevent serializing the widget if we have no input linked
|
||||
const { link } = node.inputs.find((i) => i.widget?.name === widget.name);
|
||||
if (link == null) {
|
||||
return undefined;
|
||||
}
|
||||
return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
|
||||
};
|
||||
|
||||
// Hide any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
hideWidget(node, w, ":" + widget.name);
|
||||
}
|
||||
export function hideWidget(node, widget, suffix = '') {
|
||||
widget.origType = widget.type
|
||||
widget.hidden = true
|
||||
widget.origComputeSize = widget.computeSize
|
||||
widget.origSerializeValue = widget.serializeValue
|
||||
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
|
||||
widget.type = CONVERTED_TYPE + suffix
|
||||
widget.serializeValue = () => {
|
||||
// Prevent serializing the widget if we have no input linked
|
||||
const { link } = node.inputs.find((i) => i.widget?.name === widget.name)
|
||||
if (link == null) {
|
||||
return undefined
|
||||
}
|
||||
return widget.origSerializeValue
|
||||
? widget.origSerializeValue()
|
||||
: widget.value
|
||||
}
|
||||
|
||||
// Hide any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
hideWidget(node, w, ':' + widget.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function showWidget(widget) {
|
||||
widget.type = widget.origType;
|
||||
widget.computeSize = widget.origComputeSize;
|
||||
widget.serializeValue = widget.origSerializeValue;
|
||||
widget.type = widget.origType
|
||||
widget.computeSize = widget.origComputeSize
|
||||
widget.serializeValue = widget.origSerializeValue
|
||||
|
||||
delete widget.origType;
|
||||
delete widget.origComputeSize;
|
||||
delete widget.origSerializeValue;
|
||||
delete widget.origType
|
||||
delete widget.origComputeSize
|
||||
delete widget.origSerializeValue
|
||||
|
||||
// Hide any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
showWidget(w);
|
||||
}
|
||||
// Hide any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
showWidget(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function convertToWidget(node, widget) {
|
||||
showWidget(widget);
|
||||
const sz = node.size;
|
||||
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name));
|
||||
showWidget(widget)
|
||||
const sz = node.size
|
||||
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name))
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT;
|
||||
}
|
||||
for (const widget of node.widgets) {
|
||||
widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT
|
||||
}
|
||||
|
||||
// Restore original size but grow if needed
|
||||
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
|
||||
// Restore original size but grow if needed
|
||||
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
|
||||
}
|
||||
|
||||
|
||||
export function convertToInput(node, widget, config) {
|
||||
hideWidget(node, widget);
|
||||
hideWidget(node, widget)
|
||||
|
||||
const { linkType } = getWidgetType(config);
|
||||
const { linkType } = getWidgetType(config)
|
||||
|
||||
// Add input and store widget config for creating on primitive node
|
||||
const sz = node.size;
|
||||
node.addInput(widget.name, linkType, {
|
||||
widget: { name: widget.name, config },
|
||||
});
|
||||
// Add input and store widget config for creating on primitive node
|
||||
const sz = node.size
|
||||
node.addInput(widget.name, linkType, {
|
||||
widget: { name: widget.name, config },
|
||||
})
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT;
|
||||
}
|
||||
for (const widget of node.widgets) {
|
||||
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT
|
||||
}
|
||||
|
||||
// Restore original size but grow if needed
|
||||
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
|
||||
// Restore original size but grow if needed
|
||||
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
|
||||
}
|
||||
|
||||
export function hideWidgetForGood(node, widget, suffix = "") {
|
||||
widget.origType = widget.type;
|
||||
widget.origComputeSize = widget.computeSize;
|
||||
widget.origSerializeValue = widget.serializeValue;
|
||||
widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
|
||||
widget.type = CONVERTED_TYPE + suffix;
|
||||
// widget.serializeValue = () => {
|
||||
// // Prevent serializing the widget if we have no input linked
|
||||
// const w = node.inputs?.find((i) => i.widget?.name === widget.name);
|
||||
// if (w?.link == null) {
|
||||
// return undefined;
|
||||
// }
|
||||
// return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
|
||||
// };
|
||||
export function hideWidgetForGood(node, widget, suffix = '') {
|
||||
widget.origType = widget.type
|
||||
widget.origComputeSize = widget.computeSize
|
||||
widget.origSerializeValue = widget.serializeValue
|
||||
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
|
||||
widget.type = CONVERTED_TYPE + suffix
|
||||
// widget.serializeValue = () => {
|
||||
// // Prevent serializing the widget if we have no input linked
|
||||
// const w = node.inputs?.find((i) => i.widget?.name === widget.name);
|
||||
// if (w?.link == null) {
|
||||
// return undefined;
|
||||
// }
|
||||
// return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
|
||||
// };
|
||||
|
||||
// Hide any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
hideWidgetForGood(node, w, ":" + widget.name);
|
||||
}
|
||||
// Hide any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
hideWidgetForGood(node, w, ':' + widget.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function fixWidgets(node) {
|
||||
if (node.inputs) {
|
||||
for (const input of node.inputs) {
|
||||
log(input)
|
||||
if (input.widget || node.widgets) {
|
||||
// if (newTypes.includes(input.type)) {
|
||||
const matching_widget = node.widgets.find((w) => w.name === input.name);
|
||||
if (matching_widget) {
|
||||
if (node.inputs) {
|
||||
for (const input of node.inputs) {
|
||||
log(input)
|
||||
if (input.widget || node.widgets) {
|
||||
// if (newTypes.includes(input.type)) {
|
||||
const matching_widget = node.widgets.find((w) => w.name === input.name)
|
||||
if (matching_widget) {
|
||||
// if (matching_widget.hidden) {
|
||||
// log(`Already hidden skipping ${matching_widget.name}`)
|
||||
// continue
|
||||
// }
|
||||
const w = node.widgets.find((w) => w.name === matching_widget.name)
|
||||
if (w && w.type != CONVERTED_TYPE) {
|
||||
log(w)
|
||||
log(`hidding ${w.name}(${w.type}) from ${node.type}`)
|
||||
log(node)
|
||||
hideWidget(node, w)
|
||||
} else {
|
||||
log(`converting to widget ${w}`)
|
||||
|
||||
|
||||
// if (matching_widget.hidden) {
|
||||
// log(`Already hidden skipping ${matching_widget.name}`)
|
||||
// continue
|
||||
// }
|
||||
const w = node.widgets.find((w) => w.name === matching_widget.name);
|
||||
if (w && w.type != CONVERTED_TYPE) {
|
||||
log(w)
|
||||
log(`hidding ${w.name}(${w.type}) from ${node.type}`)
|
||||
log(node)
|
||||
hideWidget(node, w);
|
||||
} else {
|
||||
log(`converting to widget ${w}`)
|
||||
|
||||
convertToWidget(node, input)
|
||||
}
|
||||
}
|
||||
}
|
||||
convertToWidget(node, input)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export function inner_value_change(widget, value, event = undefined) {
|
||||
if (widget.type == "number" || widget.type == "BBOX") {
|
||||
value = Number(value);
|
||||
} else if (widget.type == "BOOL") {
|
||||
value = Boolean(value)
|
||||
}
|
||||
widget.value = value;
|
||||
if (widget.options && widget.options.property && node.properties[widget.options.property] !== undefined) {
|
||||
node.setProperty(widget.options.property, value);
|
||||
}
|
||||
if (widget.callback) {
|
||||
widget.callback(widget.value, app.canvas, node, pos, event);
|
||||
}
|
||||
if (widget.type == 'number' || widget.type == 'BBOX') {
|
||||
value = Number(value)
|
||||
} else if (widget.type == 'BOOL') {
|
||||
value = Boolean(value)
|
||||
}
|
||||
widget.value = value
|
||||
if (
|
||||
widget.options &&
|
||||
widget.options.property &&
|
||||
node.properties[widget.options.property] !== undefined
|
||||
) {
|
||||
node.setProperty(widget.options.property, value)
|
||||
}
|
||||
if (widget.callback) {
|
||||
widget.callback(widget.value, app.canvas, node, pos, event)
|
||||
}
|
||||
}
|
||||
|
||||
//- COLOR UTILS
|
||||
export function isColorBright(rgb, threshold = 240) {
|
||||
const brightess = getBrightness(rgb)
|
||||
return brightess > threshold
|
||||
const brightess = getBrightness(rgb)
|
||||
return brightess > threshold
|
||||
}
|
||||
|
||||
function getBrightness(rgbObj) {
|
||||
return Math.round(((parseInt(rgbObj[0]) * 299) + (parseInt(rgbObj[1]) * 587) + (parseInt(rgbObj[2]) * 114)) / 1000)
|
||||
return Math.round(
|
||||
(parseInt(rgbObj[0]) * 299 +
|
||||
parseInt(rgbObj[1]) * 587 +
|
||||
parseInt(rgbObj[2]) * 114) /
|
||||
1000
|
||||
)
|
||||
}
|
||||
|
||||
//- HTML / CSS UTILS
|
||||
export function defineClass(className, classStyles) {
|
||||
const styleSheets = document.styleSheets;
|
||||
const styleSheets = document.styleSheets
|
||||
|
||||
// Helper function to check if the class exists in a style sheet
|
||||
function classExistsInStyleSheet(styleSheet) {
|
||||
const rules = styleSheet.rules || styleSheet.cssRules;
|
||||
for (const rule of rules) {
|
||||
if (rule.selectorText === `.${className}`) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
// Helper function to check if the class exists in a style sheet
|
||||
function classExistsInStyleSheet(styleSheet) {
|
||||
const rules = styleSheet.rules || styleSheet.cssRules
|
||||
for (const rule of rules) {
|
||||
if (rule.selectorText === `.${className}`) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the class is already defined in any of the style sheets
|
||||
let classExists = false;
|
||||
for (const styleSheet of styleSheets) {
|
||||
if (classExistsInStyleSheet(styleSheet)) {
|
||||
classExists = true;
|
||||
break;
|
||||
}
|
||||
// Check if the class is already defined in any of the style sheets
|
||||
let classExists = false
|
||||
for (const styleSheet of styleSheets) {
|
||||
if (classExistsInStyleSheet(styleSheet)) {
|
||||
classExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the class doesn't exist, add the new class definition to the first style sheet
|
||||
if (!classExists) {
|
||||
if (styleSheets[0].insertRule) {
|
||||
styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0);
|
||||
} else if (styleSheets[0].addRule) {
|
||||
styleSheets[0].addRule(`.${className}`, classStyles, 0);
|
||||
}
|
||||
// If the class doesn't exist, add the new class definition to the first style sheet
|
||||
if (!classExists) {
|
||||
if (styleSheets[0].insertRule) {
|
||||
styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0)
|
||||
} else if (styleSheets[0].addRule) {
|
||||
styleSheets[0].addRule(`.${className}`, classStyles, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+89
-70
@@ -1,80 +1,99 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
/**
|
||||
* File: debug.js
|
||||
* Project: comfy_mtb
|
||||
* Author: Mel Massadian
|
||||
*
|
||||
* Copyright (c) 2023 Mel Massadian
|
||||
*
|
||||
*/
|
||||
|
||||
import { app } from '/scripts/app.js'
|
||||
import * as shared from '/extensions/mtb/comfy_shared.js'
|
||||
import { log } from '/extensions/mtb/comfy_shared.js'
|
||||
import { MtbWidgets } from '/extensions/mtb/mtb_widgets.js'
|
||||
|
||||
// TODO: respect inputs order...
|
||||
|
||||
|
||||
|
||||
app.registerExtension({
|
||||
name: "mtb.Debug",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "Debug (mtb)") {
|
||||
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
||||
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
|
||||
const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined;
|
||||
// TODO: remove all widgets on disconnect once computed
|
||||
shared.dynamic_connection(this, index, connected, "anything_", "*")
|
||||
name: 'mtb.Debug',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === 'Debug (mtb)') {
|
||||
const onConnectionsChange = nodeType.prototype.onConnectionsChange
|
||||
nodeType.prototype.onConnectionsChange = function (
|
||||
type,
|
||||
index,
|
||||
connected,
|
||||
link_info
|
||||
) {
|
||||
const r = onConnectionsChange
|
||||
? onConnectionsChange.apply(this, arguments)
|
||||
: undefined
|
||||
// TODO: remove all widgets on disconnect once computed
|
||||
shared.dynamic_connection(this, index, connected, 'anything_', '*')
|
||||
|
||||
//- infer type
|
||||
if (link_info) {
|
||||
const fromNode = this.graph._nodes.find((otherNode) => otherNode.id == link_info.origin_id);
|
||||
const type = fromNode.outputs[link_info.origin_slot].type;
|
||||
this.inputs[index].type = type;
|
||||
// this.inputs[index].label = type.toLowerCase()
|
||||
}
|
||||
//- restore dynamic input
|
||||
if (!connected) {
|
||||
this.inputs[index].type = "*";
|
||||
this.inputs[index].label = `anything_${index + 1}`
|
||||
}
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
log(message)
|
||||
onExecuted?.apply(this, arguments);
|
||||
log(message)
|
||||
if (this.widgets) {
|
||||
// const pos = this.widgets.findIndex((w) => w.name === "anything_1");
|
||||
// if (pos !== -1) {
|
||||
for (let i = 0; i < this.widgets.length; i++) {
|
||||
this.widgets[i].onRemove?.();
|
||||
}
|
||||
this.widgets.length = 0;
|
||||
|
||||
}
|
||||
let widgetI = 1
|
||||
if (message.text) {
|
||||
for (const txt of message.text) {
|
||||
const w = this.addCustomWidget(MtbWidgets.DEBUG_STRING(txt, widgetI))
|
||||
w.parent = this;
|
||||
widgetI++;
|
||||
}
|
||||
}
|
||||
if (message.b64_images) {
|
||||
for (const img of message.b64_images) {
|
||||
const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, widgetI))
|
||||
w.parent = this;
|
||||
widgetI++;
|
||||
}
|
||||
// this.onResize?.(this.size);
|
||||
// this.resize?.(this.size)
|
||||
this.setSize(this.computeSize())
|
||||
};
|
||||
|
||||
this.onRemoved = function () {
|
||||
// When removing this node we need to remove the input from the DOM
|
||||
for (let y in this.widgets) {
|
||||
if (this.widgets[y].canvas) {
|
||||
this.widgets[y].canvas.remove();
|
||||
}
|
||||
this.widgets[y].onRemove?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
//- infer type
|
||||
if (link_info) {
|
||||
const fromNode = this.graph._nodes.find(
|
||||
(otherNode) => otherNode.id == link_info.origin_id
|
||||
)
|
||||
const type = fromNode.outputs[link_info.origin_slot].type
|
||||
this.inputs[index].type = type
|
||||
// this.inputs[index].label = type.toLowerCase()
|
||||
}
|
||||
//- restore dynamic input
|
||||
if (!connected) {
|
||||
this.inputs[index].type = '*'
|
||||
this.inputs[index].label = `anything_${index + 1}`
|
||||
}
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments)
|
||||
|
||||
const prefix = 'anything_'
|
||||
|
||||
if (this.widgets) {
|
||||
// const pos = this.widgets.findIndex((w) => w.name === "anything_1");
|
||||
// if (pos !== -1) {
|
||||
for (let i = 0; i < this.widgets.length; i++) {
|
||||
this.widgets[i].onRemoved?.()
|
||||
}
|
||||
this.widgets.length = 0
|
||||
}
|
||||
let widgetI = 1
|
||||
if (message.text) {
|
||||
for (const txt of message.text) {
|
||||
const w = this.addCustomWidget(
|
||||
MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, txt)
|
||||
)
|
||||
w.parent = this
|
||||
widgetI++
|
||||
}
|
||||
}
|
||||
if (message.b64_images) {
|
||||
for (const img of message.b64_images) {
|
||||
const w = this.addCustomWidget(
|
||||
MtbWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img)
|
||||
)
|
||||
w.parent = this
|
||||
widgetI++
|
||||
}
|
||||
// this.onResize?.(this.size);
|
||||
// this.resize?.(this.size)
|
||||
this.setSize(this.computeSize())
|
||||
}
|
||||
|
||||
this.onRemoved = function () {
|
||||
// When removing this node we need to remove the input from the DOM
|
||||
for (let y in this.widgets) {
|
||||
if (this.widgets[y].canvas) {
|
||||
this.widgets[y].canvas.remove()
|
||||
}
|
||||
this.widgets[y].onRemoved?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
})
|
||||
|
||||
+270
-268
@@ -1,311 +1,313 @@
|
||||
import { api } from "/scripts/api.js";
|
||||
import { app } from "/scripts/app.js";
|
||||
/**
|
||||
* File: imageFeed.js
|
||||
* Project: comfy_mtb
|
||||
* Author: Mel Massadian
|
||||
*
|
||||
* Copyright (c) 2023 Mel Massadian
|
||||
*
|
||||
*/
|
||||
|
||||
// forked from pysssss's imageFeed.js
|
||||
|
||||
const styles = {
|
||||
lighbox: {
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
background: "rgba(0,0,0,0.5)",
|
||||
display: "none",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: 999,
|
||||
},
|
||||
lightboxBtn: (extra) => ({
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "#fff",
|
||||
zIndex: 9999999,
|
||||
fontSize: "30px",
|
||||
cursor: "pointer",
|
||||
pointerEvents: "bounding-box",
|
||||
...extra,
|
||||
})
|
||||
,
|
||||
img_list: {
|
||||
import { api } from '/scripts/api.js'
|
||||
import { app } from '/scripts/app.js'
|
||||
|
||||
minHeight: "30px",
|
||||
maxHeight: "300px",
|
||||
width: "100vw",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
zIndex: 9999999,
|
||||
background: "#333",
|
||||
overflow: "auto",
|
||||
}
|
||||
const styles = {
|
||||
lighbox: {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
display: 'none',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 999,
|
||||
},
|
||||
lightboxBtn: (extra) => ({
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
zIndex: 9999999,
|
||||
fontSize: '30px',
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'bounding-box',
|
||||
...extra,
|
||||
}),
|
||||
img_list: {
|
||||
minHeight: '30px',
|
||||
maxHeight: '300px',
|
||||
width: '100vw',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
zIndex: 9999999,
|
||||
background: '#333',
|
||||
overflow: 'auto',
|
||||
},
|
||||
}
|
||||
|
||||
let currentImageIndex = 0;
|
||||
const imageUrls = [];
|
||||
let currentImageIndex = 0
|
||||
const imageUrls = []
|
||||
|
||||
let image_menu = null
|
||||
|
||||
app.registerExtension({
|
||||
name: "mtb.ImageFeed",
|
||||
setup: async () => {
|
||||
// - HTML & CSS
|
||||
//- lightbox
|
||||
const lightboxContainer = document.createElement("div");
|
||||
Object.assign(lightboxContainer.style, styles.lighbox);
|
||||
name: 'mtb.ImageFeed',
|
||||
setup: async () => {
|
||||
// - HTML & CSS
|
||||
//- lightbox
|
||||
const lightboxContainer = document.createElement('div')
|
||||
Object.assign(lightboxContainer.style, styles.lighbox)
|
||||
|
||||
const lightboxImage = document.createElement("img");
|
||||
Object.assign(lightboxImage.style, {
|
||||
maxHeight: "100%",
|
||||
maxWidth: "100%",
|
||||
borderRadius: "5px",
|
||||
});
|
||||
const lightboxImage = document.createElement('img')
|
||||
Object.assign(lightboxImage.style, {
|
||||
maxHeight: '100%',
|
||||
maxWidth: '100%',
|
||||
borderRadius: '5px',
|
||||
})
|
||||
|
||||
// previous and next buttons
|
||||
const lightboxPrevBtn = document.createElement("button");
|
||||
const lightboxNextBtn = document.createElement("button");
|
||||
// previous and next buttons
|
||||
const lightboxPrevBtn = document.createElement('button')
|
||||
const lightboxNextBtn = document.createElement('button')
|
||||
|
||||
lightboxPrevBtn.textContent = "❮";
|
||||
lightboxNextBtn.textContent = "❯";
|
||||
lightboxPrevBtn.textContent = '❮'
|
||||
lightboxNextBtn.textContent = '❯'
|
||||
|
||||
Object.assign(lightboxPrevBtn.style, styles.lightboxBtn({ left: "0%" }));
|
||||
Object.assign(lightboxNextBtn.style, styles.lightboxBtn({ right: "0%" }));
|
||||
Object.assign(lightboxPrevBtn.style, styles.lightboxBtn({ left: '0%' }))
|
||||
Object.assign(lightboxNextBtn.style, styles.lightboxBtn({ right: '0%' }))
|
||||
|
||||
// close button
|
||||
const lightboxCloseBtn = document.createElement("button");
|
||||
Object.assign(lightboxCloseBtn.style, styles.lightboxBtn({ right: "0", top: "0" }));
|
||||
lightboxCloseBtn.textContent = "❌";
|
||||
// close button
|
||||
const lightboxCloseBtn = document.createElement('button')
|
||||
Object.assign(
|
||||
lightboxCloseBtn.style,
|
||||
styles.lightboxBtn({ right: '0', top: '0' })
|
||||
)
|
||||
lightboxCloseBtn.textContent = '❌'
|
||||
|
||||
const lightboxButtons = document.createElement("div");
|
||||
Object.assign(lightboxButtons.style, {
|
||||
position: "absolute",
|
||||
top: "0%",
|
||||
right: "0%",
|
||||
// transform: "translate(50%, -50%)",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "#fff",
|
||||
fontSize: "30px",
|
||||
cursor: "pointer",
|
||||
pointerEvents: "none",
|
||||
});
|
||||
const lightboxButtons = document.createElement('div')
|
||||
Object.assign(lightboxButtons.style, {
|
||||
position: 'absolute',
|
||||
top: '0%',
|
||||
right: '0%',
|
||||
// transform: "translate(50%, -50%)",
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '30px',
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'none',
|
||||
})
|
||||
|
||||
lightboxButtons.append(lightboxPrevBtn, lightboxNextBtn, lightboxCloseBtn);
|
||||
lightboxContainer.append(lightboxButtons, lightboxImage);
|
||||
lightboxButtons.append(lightboxPrevBtn, lightboxNextBtn, lightboxCloseBtn)
|
||||
lightboxContainer.append(lightboxButtons, lightboxImage)
|
||||
|
||||
//- image list
|
||||
const imageListContainer = document.createElement("div");
|
||||
Object.assign(imageListContainer.style, styles.img_list);
|
||||
//- image list
|
||||
const imageListContainer = document.createElement('div')
|
||||
Object.assign(imageListContainer.style, styles.img_list)
|
||||
|
||||
const createImgListBtn = (text, style) => {
|
||||
const btn = document.createElement('button')
|
||||
btn.type = 'button'
|
||||
btn.textContent = text
|
||||
Object.assign(btn.style, {
|
||||
...style,
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
background: 'none',
|
||||
height: '20px',
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
top: '5px',
|
||||
fontSize: '12px',
|
||||
lineHeight: '12px',
|
||||
})
|
||||
imageListContainer.append(btn)
|
||||
return btn
|
||||
}
|
||||
const showBtn = document.createElement('button')
|
||||
const closeBtn = createImgListBtn('❌', {
|
||||
width: '20px',
|
||||
textIndent: '-4px',
|
||||
right: '5px',
|
||||
})
|
||||
const loadButton = createImgListBtn('Load Session History', {
|
||||
right: '90px',
|
||||
})
|
||||
const clearButton = createImgListBtn('Clear', {
|
||||
right: '30px',
|
||||
})
|
||||
|
||||
const createImgListBtn = (text, style) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = text;
|
||||
Object.assign(btn.style, {
|
||||
...style,
|
||||
border: "none",
|
||||
color: "#fff",
|
||||
background: "none",
|
||||
height: "20px",
|
||||
cursor: "pointer",
|
||||
position: "absolute",
|
||||
top: "5px",
|
||||
fontSize: "12px",
|
||||
lineHeight: "12px",
|
||||
});
|
||||
imageListContainer.append(btn);
|
||||
return btn;
|
||||
}
|
||||
const showBtn = document.createElement("button");
|
||||
const closeBtn = createImgListBtn("❌", {
|
||||
width: "20px",
|
||||
textIndent: "-4px",
|
||||
right: "5px",
|
||||
});
|
||||
const loadButton = createImgListBtn("Load Session History", {
|
||||
right: "90px",
|
||||
});
|
||||
const clearButton = createImgListBtn("Clear", {
|
||||
right: "30px",
|
||||
});
|
||||
//- tools popup button
|
||||
showBtn.classList.add('comfy-settings-btn')
|
||||
Object.assign(showBtn.style, {
|
||||
right: '16px',
|
||||
cursor: 'pointer',
|
||||
display: 'none',
|
||||
})
|
||||
|
||||
//- append to DOM
|
||||
document.body.append(imageListContainer)
|
||||
|
||||
//- tools popup button
|
||||
showBtn.classList.add("comfy-settings-btn");
|
||||
Object.assign(showBtn.style, {
|
||||
right: "16px",
|
||||
cursor: "pointer",
|
||||
display: "none",
|
||||
});
|
||||
showBtn.textContent = '🖼️'
|
||||
showBtn.onclick = () => {
|
||||
imageListContainer.style.display = 'block'
|
||||
showBtn.style.display = 'none'
|
||||
}
|
||||
document.querySelector('.comfy-settings-btn').after(showBtn)
|
||||
document.querySelector('.comfy-settings-btn').after(lightboxContainer)
|
||||
|
||||
//- append to DOM
|
||||
document.body.append(imageListContainer);
|
||||
// for (const { output } of history) {
|
||||
// if (output?.images) {
|
||||
// for (const src of output.images) {
|
||||
// const img = document.createElement("img");
|
||||
// const but = document.createElement("button");
|
||||
|
||||
//- callbacks
|
||||
closeBtn.onclick = () => {
|
||||
imageListContainer.style.display = 'none'
|
||||
showBtn.style.display = 'unset'
|
||||
}
|
||||
|
||||
showBtn.textContent = "🖼️";
|
||||
showBtn.onclick = () => {
|
||||
imageListContainer.style.display = "block";
|
||||
showBtn.style.display = "none";
|
||||
};
|
||||
document.querySelector(".comfy-settings-btn").after(showBtn);
|
||||
document.querySelector(".comfy-settings-btn").after(lightboxContainer);
|
||||
clearButton.onclick = () => {
|
||||
imageListContainer.replaceChildren(closeBtn, clearButton, loadButton)
|
||||
}
|
||||
|
||||
lightboxNextBtn.onclick = () => {
|
||||
currentImageIndex = (currentImageIndex + 1) % imageUrls.length
|
||||
const imageUrl = imageUrls[currentImageIndex]
|
||||
lightboxImage.src = imageUrl
|
||||
}
|
||||
|
||||
// Modify the lightboxPrevBtn onclick callback
|
||||
lightboxPrevBtn.onclick = () => {
|
||||
currentImageIndex =
|
||||
(currentImageIndex - 1 + imageUrls.length) % imageUrls.length
|
||||
const imageUrl = imageUrls[currentImageIndex]
|
||||
lightboxImage.src = imageUrl
|
||||
}
|
||||
|
||||
// for (const { output } of history) {
|
||||
// if (output?.images) {
|
||||
// for (const src of output.images) {
|
||||
// const img = document.createElement("img");
|
||||
// const but = document.createElement("button");
|
||||
lightboxCloseBtn.onclick = () => {
|
||||
lightboxContainer.style.display = 'none'
|
||||
}
|
||||
lightboxImage.onclick = lightboxNextBtn.onclick
|
||||
/**
|
||||
* This is the function that creates the image buttons for the image list
|
||||
* They are wrapped in a button so that they can be clicked and open
|
||||
* the image in the lightbox.
|
||||
* @param {*} src
|
||||
*/
|
||||
const createImageBtn = (src) => {
|
||||
console.debug(`making image ${src.filename}`)
|
||||
const img = document.createElement('img')
|
||||
const but = document.createElement('button')
|
||||
|
||||
//- callbacks
|
||||
closeBtn.onclick = () => {
|
||||
imageListContainer.style.display = "none";
|
||||
showBtn.style.display = "unset";
|
||||
};
|
||||
Object.assign(but.style, {
|
||||
height: '120px',
|
||||
width: '120px',
|
||||
})
|
||||
Object.assign(img.style, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'scale-down',
|
||||
})
|
||||
|
||||
clearButton.onclick = () => {
|
||||
imageListContainer.replaceChildren(closeBtn, clearButton, loadButton);
|
||||
}
|
||||
img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${
|
||||
src.type
|
||||
}&subfolder=${encodeURIComponent(src.subfolder)}`
|
||||
|
||||
lightboxNextBtn.onclick = () => {
|
||||
currentImageIndex = (currentImageIndex + 1) % imageUrls.length;
|
||||
const imageUrl = imageUrls[currentImageIndex];
|
||||
lightboxImage.src = imageUrl;
|
||||
};
|
||||
imageUrls.push(img.src)
|
||||
|
||||
// Modify the lightboxPrevBtn onclick callback
|
||||
lightboxPrevBtn.onclick = () => {
|
||||
currentImageIndex = (currentImageIndex - 1 + imageUrls.length) % imageUrls.length;
|
||||
const imageUrl = imageUrls[currentImageIndex];
|
||||
lightboxImage.src = imageUrl;
|
||||
};
|
||||
console.debug(img.src)
|
||||
|
||||
but.onclick = () => {
|
||||
lightboxContainer.style.display = 'flex'
|
||||
// add the same image to the lightbox
|
||||
lightboxImage.src = img.src
|
||||
// lighboxContainer.replaceChildren(lightboxButtons, img);
|
||||
}
|
||||
|
||||
lightboxCloseBtn.onclick = () => {
|
||||
lightboxContainer.style.display = "none";
|
||||
};
|
||||
lightboxImage.onclick = lightboxNextBtn.onclick;
|
||||
/**
|
||||
* This is the function that creates the image buttons for the image list
|
||||
* They are wrapped in a button so that they can be clicked and open
|
||||
* the image in the lightbox.
|
||||
* @param {*} src
|
||||
*/
|
||||
const createImageBtn = (src) => {
|
||||
console.debug(`making image ${src.filename}`);
|
||||
const img = document.createElement("img");
|
||||
const but = document.createElement("button");
|
||||
// add right click menu
|
||||
but.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
Object.assign(but.style, {
|
||||
height: "120px",
|
||||
width: "120px",
|
||||
});
|
||||
Object.assign(img.style, {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "scale-down",
|
||||
});
|
||||
if (image_menu) {
|
||||
image_menu.remove()
|
||||
}
|
||||
|
||||
img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${src.type}&subfolder=${encodeURIComponent(
|
||||
src.subfolder
|
||||
)}`;
|
||||
image_menu = document.createElement('div')
|
||||
Object.assign(image_menu.style, {
|
||||
position: 'absolute',
|
||||
top: `${e.clientY}px`,
|
||||
left: `${e.clientX}px`,
|
||||
background: '#333',
|
||||
color: '#fff',
|
||||
padding: '5px',
|
||||
borderRadius: '5px',
|
||||
zIndex: 999,
|
||||
})
|
||||
const load_img = document.createElement('button')
|
||||
load_img.textContent = 'Load'
|
||||
load_img.onclick = () => {
|
||||
app.handleFile(img.src)
|
||||
}
|
||||
|
||||
imageUrls.push(img.src);
|
||||
image_menu.appendChild(load_img)
|
||||
document.body.appendChild(image_menu)
|
||||
})
|
||||
|
||||
console.debug(img.src)
|
||||
but.append(img)
|
||||
imageListContainer.prepend(but)
|
||||
}
|
||||
|
||||
but.onclick = () => {
|
||||
lightboxContainer.style.display = "flex";
|
||||
// add the same image to the lightbox
|
||||
lightboxImage.src = img.src;
|
||||
// lighboxContainer.replaceChildren(lightboxButtons, img);
|
||||
loadButton.onclick = async () => {
|
||||
const all_history = await api.getHistory()
|
||||
for (const history of all_history.History) {
|
||||
if (history.outputs) {
|
||||
for (const key of Object.keys(history.outputs)) {
|
||||
console.debug(key)
|
||||
for (const im of history.outputs[key].images) {
|
||||
console.debug(im)
|
||||
createImageBtn(im)
|
||||
}
|
||||
}
|
||||
// for (const src of outputs.outputs.images) {
|
||||
// console.debug(src)
|
||||
// makeImage(`${src.subfolder}/${src.filename}`)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
///////-------
|
||||
|
||||
// add right click menu
|
||||
but.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
// const all_history = await api.getHistory()
|
||||
// for (const history of all_history.History) {
|
||||
// if (history.outputs) {
|
||||
// for (const key of Object.keys(history.outputs)) {
|
||||
// for (const im of history.outputs[key].images) {
|
||||
// makeImage(im)
|
||||
// }
|
||||
// }
|
||||
// // for (const src of outputs.outputs.images) {
|
||||
// // console.debug(src)
|
||||
// // makeImage(`${src.subfolder}/${src.filename}`)
|
||||
// // }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (image_menu) {
|
||||
image_menu.remove();
|
||||
}
|
||||
|
||||
image_menu = document.createElement("div");
|
||||
Object.assign(image_menu.style, {
|
||||
position: "absolute",
|
||||
top: `${e.clientY}px`,
|
||||
left: `${e.clientX}px`,
|
||||
background: "#333",
|
||||
color: "#fff",
|
||||
padding: "5px",
|
||||
borderRadius: "5px",
|
||||
zIndex: 999,
|
||||
});
|
||||
const load_img = document.createElement("button");
|
||||
load_img.textContent = "Load";
|
||||
load_img.onclick = () => {
|
||||
app.handleFile(img.src)
|
||||
|
||||
}
|
||||
|
||||
image_menu.appendChild(load_img)
|
||||
document.body.appendChild(image_menu)
|
||||
})
|
||||
|
||||
but.append(img)
|
||||
imageListContainer.prepend(but)
|
||||
};
|
||||
|
||||
|
||||
loadButton.onclick = async () => {
|
||||
const all_history = await api.getHistory();
|
||||
for (const history of all_history.History) {
|
||||
if (history.outputs) {
|
||||
for (const key of Object.keys(history.outputs)) {
|
||||
console.debug(key)
|
||||
for (const im of history.outputs[key].images) {
|
||||
console.debug(im)
|
||||
createImageBtn(im)
|
||||
}
|
||||
}
|
||||
// for (const src of outputs.outputs.images) {
|
||||
// console.debug(src)
|
||||
// makeImage(`${src.subfolder}/${src.filename}`)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////-------
|
||||
|
||||
// const all_history = await api.getHistory()
|
||||
// for (const history of all_history.History) {
|
||||
// if (history.outputs) {
|
||||
// for (const key of Object.keys(history.outputs)) {
|
||||
// for (const im of history.outputs[key].images) {
|
||||
// makeImage(im)
|
||||
// }
|
||||
// }
|
||||
// // for (const src of outputs.outputs.images) {
|
||||
// // console.debug(src)
|
||||
// // makeImage(`${src.subfolder}/${src.filename}`)
|
||||
// // }
|
||||
// }
|
||||
// }
|
||||
|
||||
//- Hook into the API
|
||||
api.addEventListener("executed", ({ detail }) => {
|
||||
if (detail?.output?.images) {
|
||||
for (const src of detail.output.images) {
|
||||
console.debug(`Adding ${src} to image feed`)
|
||||
createImageBtn(src)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
//- Hook into the API
|
||||
api.addEventListener('executed', ({ detail }) => {
|
||||
if (detail?.output?.images) {
|
||||
for (const src of detail.output.images) {
|
||||
console.debug(`Adding ${src} to image feed`)
|
||||
createImageBtn(src)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
+936
-1058
File diff suppressed because it is too large
Load Diff
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* File: notify.js
|
||||
* Project: comfy_mtb
|
||||
* Author: Mel Massadian
|
||||
*
|
||||
* Copyright (c) 2023 Mel Massadian
|
||||
*
|
||||
*/
|
||||
|
||||
import { app } from '/scripts/app.js'
|
||||
|
||||
const log = (...args) => {
|
||||
if (window.MTB?.TRACE) {
|
||||
console.debug(...args)
|
||||
}
|
||||
}
|
||||
|
||||
let transition_time = 300
|
||||
|
||||
const containerStyle = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
font-family: monospace;
|
||||
z-index: 99999;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: height ${transition_time}ms ease-in-out;
|
||||
|
||||
`
|
||||
|
||||
const toastStyle = `
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
opacity: 0;
|
||||
overflow:hidden;
|
||||
height:20px;
|
||||
transition-property: opacity, height, padding;
|
||||
transition-duration: ${transition_time}ms;
|
||||
`
|
||||
|
||||
function notify(message, timeout = 3000) {
|
||||
log('Creating toast')
|
||||
const container = document.getElementById('mtb-notify-container')
|
||||
const toast = document.createElement('div')
|
||||
toast.style.cssText = toastStyle
|
||||
toast.innerText = message
|
||||
container.appendChild(toast)
|
||||
|
||||
toast.addEventListener('transitionend', (e) => {
|
||||
// Only on out
|
||||
if (
|
||||
e.target === toast &&
|
||||
e.propertyName === 'height' &&
|
||||
e.elapsedTime > transition_time / 1000 - Number.EPSILON
|
||||
) {
|
||||
log('Transition out')
|
||||
const totalHeight = Array.from(container.children).reduce(
|
||||
(acc, child) => acc + child.offsetHeight + 10, // Add spacing of 10px between toasts
|
||||
0
|
||||
)
|
||||
container.style.height = `${totalHeight}px`
|
||||
|
||||
// If there are no toasts left, set the container's height to 0
|
||||
if (container.children.length === 0) {
|
||||
container.style.height = '0'
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
container.removeChild(toast)
|
||||
log('Removed toast from DOM')
|
||||
}, transition_time)
|
||||
} else {
|
||||
log('Transition')
|
||||
}
|
||||
})
|
||||
|
||||
// Fading in the toast
|
||||
toast.style.opacity = '1'
|
||||
|
||||
// Update container's height to fit new toast
|
||||
const totalHeight = Array.from(container.children).reduce(
|
||||
(acc, child) => acc + child.offsetHeight + 10, // Add spacing of 10px between toasts
|
||||
0
|
||||
)
|
||||
container.style.height = `${totalHeight}px`
|
||||
|
||||
// remove the toast after the specified timeout
|
||||
setTimeout(() => {
|
||||
// trigger the transitions
|
||||
toast.style.opacity = '0'
|
||||
toast.style.height = '0'
|
||||
toast.style.paddingTop = '0'
|
||||
toast.style.paddingBottom = '0'
|
||||
}, timeout - transition_time)
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'mtb.Notify',
|
||||
setup() {
|
||||
if (!window.MTB) {
|
||||
window.MTB = {}
|
||||
}
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.id = 'mtb-notify-container'
|
||||
container.style.cssText = containerStyle
|
||||
|
||||
document.body.appendChild(container)
|
||||
window.MTB.notify = notify
|
||||
// window.MTB.notify('Hello world!')
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user