Compare commits

..
18 Commits
Author SHA1 Message Date
Mel Massadian 8695cd3f1b merge: 🔀 pull request #32 from melMass/dev/next 2023-07-24 21:56:21 +02:00
melMass cf865529ab chore: 🚀 bump version 2023-07-24 21:53:36 +02:00
melMass 3b9190a69b ci: 🚀 Remove large files from release
following @WASasquatch advice
2023-07-24 21:46:44 +02:00
melMass 9a4eda3ef5 feat: 🚧 jupyter seems to require an __init__ there 2023-07-24 21:43:55 +02:00
melMass a2ecc11ebd feat: ⚡️ use notify
and push wip examples
2023-07-24 21:42:37 +02:00
melMass 7e9c97ecb4 feat: ✨ first version of Notify
This is a very simple toast notification system that I will start to
use where it makes sense. It's completely standalone and can be used
by adding it to web/extensions and then calling windows.MTB.notify(),
it even works in the console
2023-07-24 20:34:07 +02:00
melMass 3de160af25 feat: ⚡️ add an "actions" endpoint 2023-07-24 20:25:53 +02:00
melMass 3801a443bc refactor: ✨ cleaned up frontend code a bit 2023-07-24 20:20:26 +02:00
Mel Massadian bbdac97e49 docs: 📝 added lang links 2023-07-24 17:43:44 +02:00
melMass 50d51c70d0 fix: 🎨 improve a bit the HTML response of endpoints 2023-07-23 17:11:09 +02:00
melMass 55c9736a9b fix: 🐛 caching issues
Fonts and styles where searched for each rerun.
This makes it require a restart to update either but it's not a big deal
in these cases IMO.
thanks to @ltdrdata for finding this issue!
2023-07-23 16:43:25 +02:00
melMass 21729b2784 refactor: ⚡️ remove empty inits 2023-07-23 15:13:46 +02:00
melMass 8d3cc39b72 feat: ✨ add Unsplash Image node 2023-07-23 04:50:56 +02:00
melMass abf1e82adb fix: 🔥 remove notice
we don't use this anymore
2023-07-23 03:13:54 +02:00
melMass 10d05031b1 docs: 📝 add comfyforum example
Shows a lot of the new nodes but require ComfyUI-Workflow-Component
2023-07-23 01:21:20 +02:00
melMass 7142b284ad feat: ✨ add back Save Tensors 2023-07-23 01:20:06 +02:00
melMass 11128ff85a feat: ✨ add TransformImage node 2023-07-23 01:19:19 +02:00
melMass a393793cfa fix: 🔥 use BOOL everywhere 2023-07-22 20:09:31 +02:00
31 changed files with 5518 additions and 1752 deletions
+22
View File
@@ -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: |
+6
View File
@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false
}
+3
View File
@@ -0,0 +1,3 @@
extern/frame_interpolation/moment.gif
extern/frame_interpolation/photos
extern/GFPGAN/inputs
+6
View File
@@ -1,5 +1,11 @@
## MTB Nodes
**Translated Readme (using DeepTranslate, PRs are welcome)**:
![image](https://github.com/melMass/comfy_mtb/assets/7041726/f8429c14-3521-4e28-82a3-863d781976c0)
[日本語による説明](./README-JP.md)
![image](https://github.com/melMass/comfy_mtb/assets/7041726/d5cc1fdd-2820-4a5c-b2d7-482f1c222063)
[中文说明](./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
View File
@@ -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
View File
@@ -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
+932
View File
@@ -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
View File
@@ -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 {
View File
-3
View File
@@ -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
View File
@@ -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",
-12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
]
+4 -6
View File
@@ -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
View File
@@ -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,
-3
View File
@@ -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 {
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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,
]
]
+54
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+115
View File
@@ -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!')
},
})