Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84d71541c | ||
|
|
e8895028a2 | ||
|
|
b5c00450de | ||
|
|
59083391c7 | ||
|
|
8f2adacc20 | ||
|
|
089bad5560 | ||
|
|
46010e1dd5 | ||
|
|
52d876fa67 | ||
|
|
f7e7eb19d0 | ||
|
|
cb03b6718e | ||
|
|
7b734c415a | ||
|
|
64d3ec6b45 | ||
|
|
c47865ec26 | ||
|
|
b889f79baf | ||
|
|
1d8fed3534 | ||
|
|
a557788e70 | ||
|
|
05cccaffa2 | ||
|
|
85af9dd68f | ||
|
|
233615ea25 | ||
|
|
8b6aabbfaa | ||
|
|
c72539078c | ||
|
|
95fc642782 | ||
|
|
d88ca5b748 | ||
|
|
e86a484160 | ||
|
|
5d85bfd38f | ||
|
|
77eb9f9805 | ||
|
|
266e9d1024 | ||
|
|
dc7234640c | ||
|
|
4e8417d501 | ||
|
|
62609f9c6a | ||
|
|
438401b8c7 | ||
|
|
9beac36d0f | ||
|
|
313ce956fd | ||
|
|
e18d980b77 | ||
|
|
fe2d9b82b5 | ||
|
|
22858abb31 | ||
|
|
99a5f71b2c | ||
|
|
1810d4ecdb | ||
|
|
3593f0b37e | ||
|
|
858a3bcda4 | ||
|
|
484147f5b4 | ||
|
|
61a8f1123e | ||
|
|
46b056290d | ||
|
|
2cf0497823 | ||
|
|
4b5eec4e4c | ||
|
|
17c48c7d4f | ||
|
|
cd3a2ff547 |
+37
-1
@@ -2,8 +2,9 @@
|
||||
@author: BennyKok
|
||||
@title: comfyui-deploy
|
||||
@nickname: Comfy Deploy
|
||||
@description:
|
||||
@description:
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -17,19 +18,23 @@ import requests
|
||||
import folder_paths
|
||||
from folder_paths import add_model_folder_path, get_filename_list, get_folder_paths
|
||||
from tqdm import tqdm
|
||||
import re
|
||||
|
||||
from . import custom_routes
|
||||
# import routes
|
||||
|
||||
ag_path = os.path.join(os.path.dirname(__file__))
|
||||
|
||||
|
||||
def get_python_files(path):
|
||||
return [f[:-3] for f in os.listdir(path) if f.endswith(".py")]
|
||||
|
||||
|
||||
def append_to_sys_path(path):
|
||||
if path not in sys.path:
|
||||
sys.path.append(path)
|
||||
|
||||
|
||||
paths = ["comfy-nodes"]
|
||||
files = []
|
||||
|
||||
@@ -41,14 +46,45 @@ for path in paths:
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
|
||||
def split_camel_case(name):
|
||||
# Split on underscores first, then split each part on camelCase
|
||||
parts = []
|
||||
for part in name.split("_"):
|
||||
# Find all camelCase boundaries
|
||||
words = re.findall("[A-Z][^A-Z]*", part)
|
||||
if not words: # If no camelCase found, use the whole part
|
||||
words = [part]
|
||||
parts.extend(words)
|
||||
return parts
|
||||
|
||||
|
||||
# Import all the modules and append their mappings
|
||||
for file in files:
|
||||
module = importlib.import_module(file)
|
||||
|
||||
# Check if the module has explicit mappings
|
||||
if hasattr(module, "NODE_CLASS_MAPPINGS"):
|
||||
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
|
||||
if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS"):
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
|
||||
|
||||
# Auto-discover classes with ComfyUI node attributes
|
||||
for name, obj in inspect.getmembers(module):
|
||||
# Check if it's a class and has the required ComfyUI node attributes
|
||||
if (
|
||||
inspect.isclass(obj)
|
||||
and hasattr(obj, "INPUT_TYPES")
|
||||
and hasattr(obj, "RETURN_TYPES")
|
||||
):
|
||||
# Use the class name as the key if not already in mappings
|
||||
if name not in NODE_CLASS_MAPPINGS:
|
||||
NODE_CLASS_MAPPINGS[name] = obj
|
||||
# Create a display name by converting camelCase to Title Case with spaces
|
||||
words = split_camel_case(name.replace("ComfyUIDeploy", ""))
|
||||
display_name = " ".join(word.capitalize() for word in words)
|
||||
# print(display_name, name)
|
||||
NODE_DISPLAY_NAME_MAPPINGS[name] = display_name
|
||||
|
||||
WEB_DIRECTORY = "web-plugin"
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import os
|
||||
import io
|
||||
import torchaudio
|
||||
from folder_paths import get_annotated_filepath
|
||||
|
||||
|
||||
class ComfyUIDeployExternalAudio:
|
||||
RETURN_TYPES = ("AUDIO",)
|
||||
RETURN_NAMES = ("audio",)
|
||||
FUNCTION = "load_audio"
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -29,30 +28,55 @@ class ComfyUIDeployExternalAudio:
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, audio_file, **kwargs):
|
||||
return True
|
||||
|
||||
def load_audio(self, input_id, audio_file, default_value=None, display_name=None, description=None):
|
||||
if audio_file and audio_file != "":
|
||||
if audio_file.startswith(('http://', 'https://')):
|
||||
# Handle URL input
|
||||
import requests
|
||||
response = requests.get(audio_file)
|
||||
audio_data = io.BytesIO(response.content)
|
||||
waveform, sample_rate = torchaudio.load(audio_data)
|
||||
def load_audio(
|
||||
self,
|
||||
input_id,
|
||||
audio_file,
|
||||
default_value=None,
|
||||
display_name=None,
|
||||
description=None,
|
||||
):
|
||||
try:
|
||||
import torchaudio
|
||||
|
||||
if audio_file and audio_file != "":
|
||||
if audio_file.startswith(("http://", "https://")):
|
||||
# Handle URL input
|
||||
try:
|
||||
import requests
|
||||
|
||||
response = requests.get(audio_file)
|
||||
audio_data = io.BytesIO(response.content)
|
||||
waveform, sample_rate = torchaudio.load(audio_data)
|
||||
except Exception as e:
|
||||
print(f"Error loading audio from URL: {e}")
|
||||
return (default_value,)
|
||||
else:
|
||||
# Handle local file
|
||||
try:
|
||||
audio_path = get_annotated_filepath(audio_file)
|
||||
waveform, sample_rate = torchaudio.load(audio_path)
|
||||
except Exception as e:
|
||||
print(f"Error loading local audio file: {e}")
|
||||
return (default_value,)
|
||||
|
||||
audio = {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate}
|
||||
return (audio,)
|
||||
else:
|
||||
# Handle local file
|
||||
audio_path = get_annotated_filepath(audio_file)
|
||||
waveform, sample_rate = torchaudio.load(audio_path)
|
||||
|
||||
audio = {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate}
|
||||
return (audio,)
|
||||
else:
|
||||
return (default_value,)
|
||||
except ImportError as e:
|
||||
print(f"Error: torchaudio not installed or cannot be imported: {e}")
|
||||
return (default_value,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalAudio": ComfyUIDeployExternalAudio}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalAudio": "External Audio (ComfyUI Deploy)"}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ComfyUIDeployExternalAudio": "External Audio (ComfyUI Deploy)"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
class AnyType(str):
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
WILDCARD = AnyType("*")
|
||||
|
||||
class ComfyUIDeployExternalEnum:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_enum"},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"default_value": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "", "dynamic_enum": True},
|
||||
),
|
||||
"options": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (WILDCARD,)
|
||||
RETURN_NAMES = ("text",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
|
||||
def run(self, input_id, options=None, default_value=None, display_name=None, description=None):
|
||||
return [default_value]
|
||||
@@ -1,110 +0,0 @@
|
||||
import os
|
||||
import io
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import torch
|
||||
import requests
|
||||
from folder_paths import get_annotated_filepath
|
||||
|
||||
class ComfyUIDeployExternalEXR:
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask")
|
||||
FUNCTION = "load_exr"
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_exr"},
|
||||
),
|
||||
"exr_file": ("STRING", {"default": ""}),
|
||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||
},
|
||||
"optional": {
|
||||
"default_image": ("IMAGE",),
|
||||
"default_mask": ("MASK",),
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, exr_file, **kwargs):
|
||||
return True
|
||||
|
||||
def sRGBtoLinear(self, npArray):
|
||||
less = npArray <= 0.0404482362771082
|
||||
npArray[less] = npArray[less] / 12.92
|
||||
npArray[~less] = np.power((npArray[~less] + 0.055) / 1.055, 2.4)
|
||||
|
||||
def linearToSRGB(self, npArray):
|
||||
less = npArray <= 0.0031308
|
||||
npArray[less] = npArray[less] * 12.92
|
||||
npArray[~less] = np.power(npArray[~less], 1/2.4) * 1.055 - 0.055
|
||||
|
||||
def load_exr(self, input_id, exr_file, tonemap="sRGB",
|
||||
default_image=None, default_mask=None,
|
||||
display_name=None, description=None):
|
||||
try:
|
||||
if exr_file and exr_file != "":
|
||||
if exr_file.startswith(('http://', 'https://')):
|
||||
# Handle URL input
|
||||
response = requests.get(exr_file)
|
||||
# Write to temp buffer
|
||||
buffer = io.BytesIO(response.content)
|
||||
nparr = np.frombuffer(buffer.getvalue(), np.uint8)
|
||||
image = cv.imdecode(nparr, cv.IMREAD_UNCHANGED).astype(np.float32)
|
||||
else:
|
||||
# Handle local file
|
||||
exr_path = get_annotated_filepath(exr_file)
|
||||
image = cv.imread(exr_path, cv.IMREAD_UNCHANGED).astype(np.float32)
|
||||
|
||||
if len(image.shape) == 2:
|
||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
# Extract RGB and flip channels
|
||||
rgb = np.flip(image[:,:,:3], 2).copy()
|
||||
|
||||
# Apply tonemapping
|
||||
if tonemap == "sRGB":
|
||||
self.linearToSRGB(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
elif tonemap == "Reinhard":
|
||||
rgb = np.clip(rgb, 0, None)
|
||||
rgb = rgb / (rgb + 1)
|
||||
self.linearToSRGB(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
|
||||
rgb = torch.unsqueeze(torch.from_numpy(rgb), 0)
|
||||
|
||||
# Handle alpha/mask
|
||||
mask = torch.zeros((1, image.shape[0], image.shape[1]), dtype=torch.float32)
|
||||
if image.shape[2] > 3:
|
||||
mask[0] = torch.from_numpy(np.clip(image[:,:,3], 0, 1))
|
||||
|
||||
return (rgb, mask)
|
||||
else:
|
||||
# Return defaults if no file provided
|
||||
return (default_image, default_mask)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading EXR: {str(e)}")
|
||||
# Return defaults on error
|
||||
return (default_image, default_mask)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ComfyUIDeployExternalEXR": ComfyUIDeployExternalEXR
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ComfyUIDeployExternalEXR": "External EXR (ComfyUI Deploy)"
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import torch
|
||||
from folder_paths import get_annotated_filepath
|
||||
|
||||
def linear_to_srgb(np_array):
|
||||
"""Converts a linear RGB numpy array to sRGB."""
|
||||
less = np_array <= 0.0031308
|
||||
np_array[less] = np_array[less] * 12.92
|
||||
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||
return np_array
|
||||
|
||||
class ExternalExrInput:
|
||||
"""
|
||||
Node to load a single EXR image from a local file path.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"exr_file": ("STRING", {"default": "path/to/image.exr"}),
|
||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||
},
|
||||
"optional": {
|
||||
"default_image": ("IMAGE",),
|
||||
"default_mask": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def run(self, exr_file, tonemap, default_image=None, default_mask=None):
|
||||
image = None
|
||||
try:
|
||||
if exr_file and exr_file.strip() != "":
|
||||
exr_path = get_annotated_filepath(exr_file)
|
||||
if os.path.exists(exr_path):
|
||||
image = cv.imread(exr_path, cv.IMREAD_UNCHANGED).astype(np.float32)
|
||||
else:
|
||||
print(f"Warning: File not found at {exr_path}")
|
||||
|
||||
if image is None:
|
||||
raise ValueError("Image could not be loaded.")
|
||||
|
||||
if len(image.shape) == 2: # Grayscale
|
||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
rgb = np.flip(image[:, :, :3], 2).copy() # BGR to RGB
|
||||
|
||||
# Apply tonemapping
|
||||
if tonemap == "sRGB":
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
elif tonemap == "Reinhard":
|
||||
rgb = np.clip(rgb, 0, None)
|
||||
rgb = rgb / (rgb + 1)
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
|
||||
rgb_tensor = torch.from_numpy(rgb).unsqueeze(0)
|
||||
|
||||
# Handle alpha/mask
|
||||
if image.shape[2] > 3:
|
||||
mask = np.clip(image[:, :, 3], 0, 1)
|
||||
else:
|
||||
mask = np.ones_like(rgb[:, :, 0])
|
||||
mask_tensor = torch.from_numpy(mask).unsqueeze(0)
|
||||
|
||||
return (rgb_tensor, mask_tensor)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading EXR file '{exr_file}': {e}")
|
||||
if default_image is not None and default_mask is not None:
|
||||
print("Returning default image.")
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: Error loading EXR and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ExternalExrInput": ExternalExrInput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ExternalExrInput": "External EXR Input (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import torch
|
||||
import numpy as np
|
||||
import folder_paths
|
||||
|
||||
def srgb_to_linear(np_array):
|
||||
"""Converts an sRGB numpy array to linear RGB."""
|
||||
less = np_array <= 0.0404482362771082
|
||||
np_array[less] = np_array[less] / 12.92
|
||||
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||
return np_array
|
||||
|
||||
class ExternalExrOutput:
|
||||
"""
|
||||
Node to save a single image as an EXR file to a local path.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "output"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filepath": ("STRING", {"default": "/tmp/output.exr"}),
|
||||
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def run(self, images, filepath, tonemap):
|
||||
if not filepath.endswith(".exr"):
|
||||
raise ValueError("Filepath must end with '.exr'")
|
||||
|
||||
output_dir = os.path.dirname(filepath)
|
||||
if not os.path.isabs(output_dir):
|
||||
raise ValueError("Filepath must be an absolute path.")
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# We only process the first image in the batch
|
||||
image_tensor = images[0]
|
||||
|
||||
linear = image_tensor.cpu().numpy().astype(np.float32)
|
||||
|
||||
# If the source is sRGB, convert to linear
|
||||
if tonemap == "sRGB":
|
||||
linear[...,:3] = srgb_to_linear(linear[...,:3])
|
||||
|
||||
# Convert RGB to BGR for OpenCV
|
||||
bgr = np.flip(linear, 2).copy()
|
||||
|
||||
# Save the image
|
||||
cv.imwrite(filepath, bgr)
|
||||
|
||||
print(f"Saved EXR file to: {filepath}")
|
||||
|
||||
return {"ui": {"images": [{"filename": os.path.basename(filepath), "subfolder": os.path.dirname(filepath), "type": self.type}]}}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ExternalExrOutput": ExternalExrOutput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ExternalExrOutput": "External EXR Output (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import torch
|
||||
import re
|
||||
from folder_paths import get_annotated_filepath
|
||||
|
||||
def linear_to_srgb(np_array):
|
||||
"""Converts a linear RGB numpy array to sRGB."""
|
||||
less = np_array <= 0.0031308
|
||||
np_array[less] = np_array[less] * 12.92
|
||||
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||
return np_array
|
||||
|
||||
class ExternalExrSequenceInput:
|
||||
"""
|
||||
Node to load a sequence of EXR images from a local filepath pattern, a directory,
|
||||
or a single file within a sequence.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"path_or_pattern": ("STRING", {"default": "path/to/frames_or_pattern"}),
|
||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||
"start_frame": ("INT", {"default": 1, "min": 1}),
|
||||
"end_frame": ("INT", {"default": 50, "min": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"default_image": ("IMAGE",),
|
||||
"default_mask": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def get_image_paths(self, path_input, start_frame, end_frame):
|
||||
image_paths = []
|
||||
|
||||
# Case 1: Input is a C-style pattern
|
||||
if '%' in path_input:
|
||||
print(f"Pattern detected: {path_input}")
|
||||
for i in range(start_frame, end_frame + 1):
|
||||
fpath = get_annotated_filepath(path_input % i)
|
||||
if os.path.exists(fpath):
|
||||
image_paths.append(fpath)
|
||||
return image_paths
|
||||
|
||||
annotated_path = get_annotated_filepath(path_input)
|
||||
|
||||
# Case 2: Input is a directory
|
||||
if os.path.isdir(annotated_path):
|
||||
print(f"Directory detected: {annotated_path}")
|
||||
files_in_dir = sorted(os.listdir(annotated_path))
|
||||
for filename in files_in_dir:
|
||||
if not filename.lower().endswith('.exr'):
|
||||
continue
|
||||
|
||||
matches = re.findall(r'\d+', filename)
|
||||
if not matches:
|
||||
continue
|
||||
|
||||
frame_number = int(matches[-1])
|
||||
if start_frame <= frame_number <= end_frame:
|
||||
image_paths.append(os.path.join(annotated_path, filename))
|
||||
return image_paths
|
||||
|
||||
# Case 3: Input is a single file from a sequence
|
||||
if os.path.isfile(annotated_path):
|
||||
print(f"Single file detected: {annotated_path}. Attempting to find sequence.")
|
||||
base_dir = os.path.dirname(annotated_path)
|
||||
filename = os.path.basename(annotated_path)
|
||||
|
||||
matches = list(re.finditer(r'(\d+)', filename))
|
||||
if not matches: # It's a single file with no frame number
|
||||
return [annotated_path]
|
||||
|
||||
last_match = matches[-1]
|
||||
num_start_pos, num_end_pos = last_match.span()
|
||||
prefix = filename[:num_start_pos]
|
||||
suffix = filename[num_end_pos:]
|
||||
padding = len(last_match.group(0))
|
||||
|
||||
for i in range(start_frame, end_frame + 1):
|
||||
potential_filename = f"{prefix}{str(i).zfill(padding)}{suffix}"
|
||||
potential_path = os.path.join(base_dir, potential_filename)
|
||||
if os.path.exists(potential_path):
|
||||
image_paths.append(potential_path)
|
||||
return image_paths
|
||||
|
||||
return [] # Return empty if no cases match
|
||||
|
||||
def run(self, path_or_pattern, tonemap, start_frame, end_frame, default_image=None, default_mask=None):
|
||||
try:
|
||||
image_paths = self.get_image_paths(path_or_pattern, start_frame, end_frame)
|
||||
if not image_paths:
|
||||
raise ValueError(f"No EXR files found for '{path_or_pattern}' between frames {start_frame}-{end_frame}.")
|
||||
|
||||
print(f"Found {len(image_paths)} EXR files to load.")
|
||||
rgb_frames = []
|
||||
mask_frames = []
|
||||
|
||||
for path in image_paths:
|
||||
image = cv.imread(path, cv.IMREAD_UNCHANGED)
|
||||
if image is None:
|
||||
print(f"Warning: Could not read file {path}, skipping.")
|
||||
continue
|
||||
|
||||
image = image.astype(np.float32)
|
||||
if len(image.shape) == 2:
|
||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
rgb = np.flip(image[:, :, :3], 2).copy()
|
||||
|
||||
if tonemap == "sRGB":
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
elif tonemap == "Reinhard":
|
||||
rgb = np.clip(rgb, 0, None)
|
||||
rgb = rgb / (rgb + 1)
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
|
||||
rgb_frames.append(torch.from_numpy(rgb))
|
||||
|
||||
if image.shape[2] > 3:
|
||||
mask = np.clip(image[:, :, 3], 0, 1)
|
||||
else:
|
||||
mask = np.ones_like(rgb[:, :, 0])
|
||||
mask_frames.append(torch.from_numpy(mask))
|
||||
|
||||
if not rgb_frames:
|
||||
raise ValueError("No frames were loaded successfully.")
|
||||
|
||||
print(f"Successfully loaded {len(rgb_frames)} frames into a batch.")
|
||||
return (torch.stack(rgb_frames, 0), torch.stack(mask_frames, 0))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading EXR sequence: {e}")
|
||||
if default_image is not None and default_mask is not None:
|
||||
print("Returning default image.")
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: Error loading sequence and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ExternalExrSequenceInput": ExternalExrSequenceInput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ExternalExrSequenceInput": "External EXR Sequence Input (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import torch
|
||||
import numpy as np
|
||||
import re
|
||||
|
||||
def srgb_to_linear(np_array):
|
||||
"""Converts an sRGB numpy array to linear RGB."""
|
||||
less = np_array <= 0.0404482362771082
|
||||
np_array[less] = np_array[less] / 12.92
|
||||
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||
return np_array
|
||||
|
||||
class ExternalExrSequenceOutput:
|
||||
"""
|
||||
Node to save a sequence of images as EXR files to a local directory.
|
||||
It uses a filepath pattern like 'path/to/frame_%04d.exr' to save each frame.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.type = "output"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filepath_pattern": ("STRING", {"default": "/tmp/exr_sequence/frame_%04d.exr"}),
|
||||
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def run(self, images, filepath_pattern, tonemap):
|
||||
# Basic validation for the filepath pattern
|
||||
if not re.search(r'%0?\d+d', filepath_pattern):
|
||||
raise ValueError("Filepath pattern must contain a C-style format specifier like '%04d'.")
|
||||
|
||||
if not filepath_pattern.endswith(".exr"):
|
||||
raise ValueError("Filepath pattern must end with '.exr'.")
|
||||
|
||||
output_dir = os.path.dirname(filepath_pattern)
|
||||
if not os.path.isabs(output_dir):
|
||||
raise ValueError("Filepath must be an absolute path.")
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Convert tensor to numpy array
|
||||
linear_images = images.cpu().numpy().astype(np.float32)
|
||||
|
||||
# If the source is sRGB, convert to linear
|
||||
if tonemap == "sRGB":
|
||||
srgb_to_linear(linear_images[...,:3])
|
||||
|
||||
# Convert RGB to BGR for OpenCV
|
||||
bgr_images = np.flip(linear_images, 3).copy()
|
||||
|
||||
results = []
|
||||
for i, bgr_image in enumerate(bgr_images):
|
||||
frame_num = i + 1
|
||||
try:
|
||||
# Use the pattern to format the full file path
|
||||
file_path = filepath_pattern % frame_num
|
||||
except TypeError:
|
||||
raise ValueError("Invalid format specifier in filepath_pattern. Use '%d', '%04d', etc.")
|
||||
|
||||
# Save the image
|
||||
cv.imwrite(file_path, bgr_image)
|
||||
|
||||
results.append({
|
||||
"filename": os.path.basename(file_path),
|
||||
"subfolder": os.path.dirname(file_path),
|
||||
"type": self.type,
|
||||
})
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ExternalExrSequenceOutput": ExternalExrSequenceOutput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ExternalExrSequenceOutput": "External EXR Sequence Output (ComfyDeploy)"
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
import folder_paths
|
||||
from PIL import Image, ImageOps
|
||||
import numpy as np
|
||||
import torch
|
||||
import folder_paths
|
||||
|
||||
|
||||
class AnyType(str):
|
||||
@@ -41,6 +37,10 @@ class ComfyUIDeployExternalLora:
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"bearer_token": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class ComfyUIDeployExternalLora:
|
||||
display_name=None,
|
||||
description=None,
|
||||
lora_url=None,
|
||||
bearer_token=None,
|
||||
):
|
||||
import requests
|
||||
import os
|
||||
@@ -84,9 +85,13 @@ class ComfyUIDeployExternalLora:
|
||||
+ " to "
|
||||
+ destination_path
|
||||
)
|
||||
headers = {"User-Agent": "Mozilla/5.0"}
|
||||
if bearer_token:
|
||||
headers["Authorization"] = f"Bearer {bearer_token}"
|
||||
print("using bearer token")
|
||||
response = requests.get(
|
||||
lora_url,
|
||||
headers={"User-Agent": "Mozilla/5.0"},
|
||||
headers=headers,
|
||||
allow_redirects=True,
|
||||
)
|
||||
with open(destination_path, "wb") as out_file:
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
class ComfyUIDeployExternalNumberSliderInt:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_number_slider_int"},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"default_value": (
|
||||
"INT",
|
||||
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 1, "step": 1},
|
||||
),
|
||||
"min_value": (
|
||||
"INT",
|
||||
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0, "step": 1},
|
||||
),
|
||||
"max_value": (
|
||||
"INT",
|
||||
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 10, "step": 1},
|
||||
),
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ("value",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
|
||||
def run(self, input_id, default_value=None, min_value=0, max_value=10, display_name=None, description=None):
|
||||
try:
|
||||
int_value = int(round(float(input_id)))
|
||||
if min_value <= int_value <= max_value:
|
||||
print("my integer", int_value)
|
||||
return [int_value]
|
||||
else:
|
||||
print("Integer out of range. Returning default value:", default_value)
|
||||
return [default_value]
|
||||
except (ValueError, TypeError):
|
||||
print("Invalid input. Returning default value:", default_value)
|
||||
return [default_value]
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalNumberSliderInt": ComfyUIDeployExternalNumberSliderInt}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalNumberSliderInt": "External Number Slider Int (ComfyUI Deploy)"}
|
||||
@@ -0,0 +1,116 @@
|
||||
import random
|
||||
|
||||
|
||||
class ComfyUIDeployExternalSeed:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_seed"},
|
||||
),
|
||||
"default_value": (
|
||||
"INT",
|
||||
{"default": -1},
|
||||
),
|
||||
"min_value": (
|
||||
"INT",
|
||||
{"default": 1, "min": 1, "max": 999999999999999},
|
||||
),
|
||||
"max_value": (
|
||||
"INT",
|
||||
{"default": 4294967295, "min": 1, "max": 999999999999999},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": True,
|
||||
"default": 'For default value:\n"-1" (i.e. not in range): Randomize within the min and max value range. \nin range: Fixed, always the same value\n',
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ("seed",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
|
||||
# Limits
|
||||
_MAX_LIMIT = 999_999_999_999_999 # 15 digits
|
||||
|
||||
# Store cached seed when fixed flag is enabled
|
||||
_cached_seed = None
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(
|
||||
cls,
|
||||
input_id,
|
||||
min_value,
|
||||
max_value,
|
||||
default_value=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Inform ComfyUI whether the node output should be considered changed.
|
||||
|
||||
If default_value is within range (Fixed mode), we return the inputs tuple
|
||||
so the cached result is reused until the user changes something.
|
||||
For Randomize mode, we force re-execution each queue.
|
||||
"""
|
||||
# Clamp values to allowed range for check
|
||||
min_value = max(1, min_value)
|
||||
max_value = min(cls._MAX_LIMIT, max_value)
|
||||
|
||||
# Fixed mode when default_value is within range
|
||||
if (
|
||||
default_value is not None
|
||||
and default_value >= min_value
|
||||
and default_value <= max_value
|
||||
):
|
||||
return (input_id, default_value)
|
||||
|
||||
# For Randomize (default_value is -1 or out of range) we force re-execution
|
||||
import random as _rnd
|
||||
|
||||
return _rnd.random()
|
||||
|
||||
def run(
|
||||
self,
|
||||
input_id,
|
||||
min_value: int,
|
||||
max_value: int,
|
||||
display_name=None,
|
||||
description=None,
|
||||
default_value: int = -1,
|
||||
):
|
||||
# Clamp values to allowed range
|
||||
min_value = max(1, min_value)
|
||||
max_value = min(self._MAX_LIMIT, max_value)
|
||||
|
||||
# Ensure limits are in correct order after clamping
|
||||
if min_value > max_value:
|
||||
min_value, max_value = max_value, min_value
|
||||
|
||||
# Fixed mode: default_value is within range
|
||||
if default_value >= min_value and default_value <= max_value:
|
||||
seed = int(default_value)
|
||||
self._cached_seed = seed
|
||||
return [seed]
|
||||
|
||||
# Randomize mode: default_value is -1 or out of range
|
||||
seed = random.randint(min_value, max_value)
|
||||
self._cached_seed = seed
|
||||
return [seed]
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalSeed": ComfyUIDeployExternalSeed}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ComfyUIDeployExternalSeed": "External Seed (ComfyUI Deploy)"
|
||||
}
|
||||
@@ -748,36 +748,64 @@ class ComfyUIDeployExternalVideo:
|
||||
file_parts = f.split(".")
|
||||
if len(file_parts) > 1 and (file_parts[-1] in video_extensions):
|
||||
files.append(f)
|
||||
return {"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_video"},
|
||||
),
|
||||
"force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1}),
|
||||
"force_size": (["Disabled", "Custom Height", "Custom Width", "Custom", "256x?", "?x256", "256x256", "512x?", "?x512", "512x512"],),
|
||||
"custom_width": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
|
||||
"custom_height": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
|
||||
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
|
||||
"skip_first_frames": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
|
||||
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"meta_batch": ("VHS_BatchManager",),
|
||||
"vae": ("VAE",),
|
||||
"default_video": (sorted(files),),
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID"
|
||||
},
|
||||
}
|
||||
return {
|
||||
"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_video"},
|
||||
),
|
||||
"force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1}),
|
||||
"force_size": (
|
||||
[
|
||||
"Disabled",
|
||||
"Custom Height",
|
||||
"Custom Width",
|
||||
"Custom",
|
||||
"256x?",
|
||||
"?x256",
|
||||
"256x256",
|
||||
"512x?",
|
||||
"?x512",
|
||||
"512x512",
|
||||
],
|
||||
),
|
||||
"custom_width": (
|
||||
"INT",
|
||||
{"default": 512, "min": 0, "max": DIMMAX, "step": 8},
|
||||
),
|
||||
"custom_height": (
|
||||
"INT",
|
||||
{"default": 512, "min": 0, "max": DIMMAX, "step": 8},
|
||||
),
|
||||
"frame_load_cap": (
|
||||
"INT",
|
||||
{"default": 0, "min": 0, "max": BIGMAX, "step": 1},
|
||||
),
|
||||
"skip_first_frames": (
|
||||
"INT",
|
||||
{"default": 0, "min": 0, "max": BIGMAX, "step": 1},
|
||||
),
|
||||
"select_every_nth": (
|
||||
"INT",
|
||||
{"default": 1, "min": 1, "max": BIGMAX, "step": 1},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"meta_batch": ("VHS_BatchManager",),
|
||||
"vae": ("VAE",),
|
||||
"default_video": (sorted(files),),
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"default_value_url": ("STRING", {"image_preview": True, "default": ""}),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
|
||||
|
||||
@@ -804,16 +832,21 @@ class ComfyUIDeployExternalVideo:
|
||||
select_every_nth = kwargs.get("select_every_nth")
|
||||
meta_batch = kwargs.get("meta_batch")
|
||||
unique_id = kwargs.get("unique_id")
|
||||
|
||||
default_value_url = kwargs.get("default_value_url")
|
||||
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
if input_id.startswith("http"):
|
||||
if input_id.startswith("http") or (
|
||||
default_value_url and default_value_url.startswith("http")
|
||||
):
|
||||
import requests
|
||||
|
||||
print("Fetching video from URL: ", input_id)
|
||||
response = requests.get(input_id, stream=True)
|
||||
# Use input_id if it's a URL, otherwise use default_value_url
|
||||
url = input_id if input_id.startswith("http") else default_value_url
|
||||
|
||||
print("Fetching video from URL: ", url)
|
||||
response = requests.get(url, stream=True)
|
||||
file_size = int(response.headers.get("Content-Length", 0))
|
||||
file_extension = input_id.split(".")[-1].split("?")[
|
||||
file_extension = url.split(".")[-1].split("?")[
|
||||
0
|
||||
] # Extract extension and handle URLs with parameters
|
||||
if file_extension not in video_extensions:
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import torch
|
||||
import requests
|
||||
|
||||
def linear_to_srgb(np_array):
|
||||
"""Converts a linear RGB numpy array to sRGB."""
|
||||
less = np_array <= 0.0031308
|
||||
np_array[less] = np_array[less] * 12.92
|
||||
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||
return np_array
|
||||
|
||||
class HttpExrInput:
|
||||
"""
|
||||
Node to load a single EXR image from a URL, with optional tonemapping.
|
||||
This node is designed to be used in a ComfyDeploy environment where input files are provided via signed URLs.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"get_signed_url": ("STRING", {"multiline": True, "default": ""}),
|
||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"optional": {
|
||||
"default_image": ("IMAGE",),
|
||||
"default_mask": ("MASK",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def load_exr_from_data(self, exr_data):
|
||||
try:
|
||||
nparr = np.frombuffer(exr_data, np.uint8)
|
||||
# Use cv.IMREAD_UNCHANGED to keep all channels (e.g., alpha)
|
||||
image = cv.imdecode(nparr, cv.IMREAD_UNCHANGED)
|
||||
if image is None:
|
||||
raise ValueError("Failed to decode EXR data.")
|
||||
return image.astype(np.float32)
|
||||
except Exception as e:
|
||||
print(f"Error decoding EXR data: {e}")
|
||||
return None
|
||||
|
||||
def run(self, get_signed_url, tonemap, seed, default_image=None, default_mask=None):
|
||||
if not get_signed_url or get_signed_url.strip() == "":
|
||||
print("Warning: No input URL provided. Returning default image if available.")
|
||||
if default_image is not None and default_mask is not None:
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: No input URL and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
image = None
|
||||
try:
|
||||
print(f"Fetching EXR from URL: {get_signed_url}")
|
||||
response = requests.get(get_signed_url)
|
||||
response.raise_for_status()
|
||||
image = self.load_exr_from_data(response.content)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching EXR from URL {get_signed_url}: {e}")
|
||||
|
||||
if image is None:
|
||||
print("Warning: Could not load or decode EXR image. Returning default image if available.")
|
||||
if default_image is not None and default_mask is not None:
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: Failed to load EXR and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
# BGR to RGB conversion and channel handling
|
||||
if len(image.shape) == 2: # Grayscale
|
||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
rgb = np.flip(image[:, :, :3], 2).copy() # OpenCV loads as BGR, convert to RGB
|
||||
|
||||
# Tonemapping
|
||||
if tonemap == "sRGB":
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
elif tonemap == "Reinhard":
|
||||
rgb = np.clip(rgb, 0, None) # Ensure no negative values
|
||||
rgb = rgb / (rgb + 1)
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
|
||||
# Handle alpha channel if it exists
|
||||
if image.shape[2] > 3:
|
||||
mask = np.clip(image[:, :, 3], 0, 1)
|
||||
else:
|
||||
mask = np.ones_like(rgb[:, :, 0]) # Create a full white mask if no alpha
|
||||
|
||||
return (torch.from_numpy(rgb).unsqueeze(0), torch.from_numpy(mask).unsqueeze(0),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"HttpExrInput": HttpExrInput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"HttpExrInput": "HTTP EXR Input (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import torch
|
||||
import numpy as np
|
||||
import requests
|
||||
|
||||
def srgb_to_linear(np_array):
|
||||
"""Converts an sRGB numpy array to linear RGB."""
|
||||
less = np_array <= 0.0404482362771082
|
||||
np_array[less] = np_array[less] / 12.92
|
||||
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||
return np_array
|
||||
|
||||
class HttpExrOutput:
|
||||
"""
|
||||
Node to save a single EXR image to a pre-signed URL.
|
||||
This node is designed for ComfyDeploy to upload the generated EXR file.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"put_signed_url": ("STRING", {"multiline": True, "default": ""}),
|
||||
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "ComfyDeploy/EXR"
|
||||
|
||||
def run(self, images, put_signed_url, tonemap, prompt=None, extra_pnginfo=None):
|
||||
if not put_signed_url or put_signed_url.strip() == "":
|
||||
print("Warning: No put_signed_url provided. Nothing will be uploaded.")
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
# We process only the first image of the batch
|
||||
image_tensor = images[0]
|
||||
|
||||
# Convert tensor to numpy array, assuming it's in range [0, 1]
|
||||
linear = image_tensor.cpu().numpy().astype(np.float32)
|
||||
|
||||
# If the source is sRGB, convert to linear
|
||||
if tonemap == "sRGB":
|
||||
linear[...,:3] = srgb_to_linear(linear[...,:3])
|
||||
|
||||
# Convert RGB to BGR for OpenCV
|
||||
bgr = np.flip(linear, 2).copy()
|
||||
|
||||
results = []
|
||||
try:
|
||||
# Encode the image to the EXR format in memory
|
||||
is_success, buffer = cv.imencode(".exr", bgr)
|
||||
if not is_success:
|
||||
raise Exception("Failed to encode image to EXR format.")
|
||||
|
||||
# Upload the image data to the pre-signed URL
|
||||
response = requests.put(put_signed_url, data=buffer.tobytes(), headers={'Content-Type': 'image/x-exr'})
|
||||
response.raise_for_status()
|
||||
|
||||
print(f"Successfully uploaded EXR to: {put_signed_url}")
|
||||
# The UI can optionally display a link or confirmation
|
||||
results.append({"url": put_signed_url, "output_id": "output_http_exr"})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error uploading EXR to signed URL: {e}")
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"HttpExrOutput": HttpExrOutput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"HttpExrOutput": "HTTP EXR Output (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import torch
|
||||
import requests
|
||||
import json
|
||||
|
||||
def linear_to_srgb(np_array):
|
||||
"""Converts a linear RGB numpy array to sRGB."""
|
||||
less = np_array <= 0.0031308
|
||||
np_array[less] = np_array[less] * 12.92
|
||||
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||
return np_array
|
||||
|
||||
class HttpExrSequenceInput:
|
||||
"""
|
||||
Node to load a sequence of EXR images from a list of URLs provided as a JSON string.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"urls_json": ("STRING", {"multiline": True, "default": "[]"}),
|
||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"optional": {
|
||||
"default_image": ("IMAGE",),
|
||||
"default_mask": ("MASK",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def load_exr_from_data(self, exr_data):
|
||||
try:
|
||||
nparr = np.frombuffer(exr_data, np.uint8)
|
||||
image = cv.imdecode(nparr, cv.IMREAD_UNCHANGED)
|
||||
if image is None:
|
||||
raise ValueError("Failed to decode EXR data.")
|
||||
return image.astype(np.float32)
|
||||
except Exception as e:
|
||||
print(f"Error decoding EXR data: {e}")
|
||||
return None
|
||||
|
||||
def run(self, urls_json, tonemap, seed, default_image=None, default_mask=None):
|
||||
try:
|
||||
urls = json.loads(urls_json)
|
||||
if not isinstance(urls, list) or not all(isinstance(u, str) for u in urls):
|
||||
raise ValueError("urls_json must be a JSON array of URL strings.")
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
print(f"Error parsing urls_json: {e}. Using default image if available.")
|
||||
urls = []
|
||||
|
||||
if not urls:
|
||||
if default_image is not None and default_mask is not None:
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: No valid URLs and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
rgb_frames = []
|
||||
mask_frames = []
|
||||
|
||||
for url in urls:
|
||||
image = None
|
||||
try:
|
||||
print(f"Fetching EXR from URL: {url}")
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
image = self.load_exr_from_data(response.content)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching EXR from URL {url}: {e}")
|
||||
|
||||
if image is None:
|
||||
print(f"Warning: Could not decode EXR from {url}. Skipping frame.")
|
||||
continue
|
||||
|
||||
if len(image.shape) == 2: # Grayscale
|
||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
rgb = np.flip(image[:, :, :3], 2).copy() # BGR to RGB
|
||||
|
||||
if tonemap == "sRGB":
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
elif tonemap == "Reinhard":
|
||||
rgb = np.clip(rgb, 0, None)
|
||||
rgb = rgb / (rgb + 1)
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
|
||||
rgb_frames.append(torch.from_numpy(rgb))
|
||||
|
||||
if image.shape[2] > 3:
|
||||
mask = np.clip(image[:, :, 3], 0, 1)
|
||||
else:
|
||||
mask = np.ones_like(rgb[:, :, 0])
|
||||
mask_frames.append(torch.from_numpy(mask))
|
||||
|
||||
if not rgb_frames:
|
||||
print("Could not load any frames. Returning default image if available.")
|
||||
if default_image is not None and default_mask is not None:
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: Failed to load any frames and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
print(f"Loaded {len(rgb_frames)} frames successfully.")
|
||||
return (torch.stack(rgb_frames, 0), torch.stack(mask_frames, 0))
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"HttpExrSequenceInput": HttpExrSequenceInput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"HttpExrSequenceInput": "HTTP EXR Sequence Input (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import torch
|
||||
import numpy as np
|
||||
import requests
|
||||
import json
|
||||
|
||||
def srgb_to_linear(np_array):
|
||||
"""Converts an sRGB numpy array to linear RGB."""
|
||||
less = np_array <= 0.0404482362771082
|
||||
np_array[less] = np_array[less] / 12.92
|
||||
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||
return np_array
|
||||
|
||||
class HttpExrSequenceOutput:
|
||||
"""
|
||||
Node to save a sequence of images as EXR files to a list of pre-signed URLs.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.type = "output"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"upload_urls_json": ("STRING", {"multiline": True, "default": "[]"}),
|
||||
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def run(self, images, upload_urls_json, tonemap):
|
||||
try:
|
||||
upload_urls = json.loads(upload_urls_json)
|
||||
if not isinstance(upload_urls, list) or not all(isinstance(u, str) for u in upload_urls):
|
||||
raise ValueError("upload_urls_json must be a JSON array of URL strings.")
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
print(f"Error parsing upload_urls_json: {e}. Aborting upload.")
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
if not upload_urls:
|
||||
print("Warning: No upload URLs provided. Nothing will be uploaded.")
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
if len(images) != len(upload_urls):
|
||||
print(f"Warning: Mismatch between number of images ({len(images)}) and upload URLs ({len(upload_urls)}). Aborting upload.")
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
# Convert tensor to numpy array
|
||||
linear_images = images.cpu().numpy().astype(np.float32)
|
||||
|
||||
# If the source is sRGB, convert all images to linear
|
||||
if tonemap == "sRGB":
|
||||
srgb_to_linear(linear_images[...,:3])
|
||||
|
||||
# Convert RGB to BGR for OpenCV
|
||||
bgr_images = np.flip(linear_images, 3).copy()
|
||||
|
||||
results = []
|
||||
for i, (bgr_image, url) in enumerate(zip(bgr_images, upload_urls)):
|
||||
try:
|
||||
# Encode the image to the EXR format in memory
|
||||
is_success, buffer = cv.imencode(".exr", bgr_image)
|
||||
if not is_success:
|
||||
raise Exception("Failed to encode image to EXR format.")
|
||||
|
||||
# Upload the image data to the pre-signed URL
|
||||
response = requests.put(url, data=buffer.tobytes(), headers={'Content-Type': 'image/x-exr'})
|
||||
response.raise_for_status()
|
||||
|
||||
print(f"Successfully uploaded frame {i+1} to: {url}")
|
||||
results.append({"url": url})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error uploading frame {i+1} to {url}: {e}")
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"HttpExrSequenceOutput": HttpExrSequenceOutput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"HttpExrSequenceOutput": "HTTP EXR Sequence Output (ComfyDeploy)"
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import folder_paths
|
||||
class AnyType(str):
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
from os import walk
|
||||
|
||||
WILDCARD = AnyType("*")
|
||||
|
||||
MODEL_EXTENSIONS = {
|
||||
"safetensors": "SafeTensors file format",
|
||||
"ckpt": "Checkpoint file",
|
||||
"pth": "PyTorch serialized file",
|
||||
"pkl": "Pickle file",
|
||||
"onnx": "ONNX file",
|
||||
}
|
||||
|
||||
def fetch_files(path):
|
||||
for (dirpath, dirnames, filenames) in walk(path):
|
||||
fs = []
|
||||
if len(dirnames) > 0:
|
||||
for dirname in dirnames:
|
||||
fs.extend(fetch_files(f"{dirpath}/{dirname}"))
|
||||
for filename in filenames:
|
||||
# Remove "./models/" from the beginning of dirpath
|
||||
relative_dirpath = dirpath.replace("./models/", "", 1)
|
||||
file_path = f"{relative_dirpath}/{filename}"
|
||||
|
||||
# Only add files that are known model extensions
|
||||
file_extension = filename.split('.')[-1].lower()
|
||||
if file_extension in MODEL_EXTENSIONS:
|
||||
fs.append(file_path)
|
||||
|
||||
return fs
|
||||
allModels = fetch_files("./models")
|
||||
|
||||
class ComfyUIDeployModalList:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"model": (allModels, ),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (WILDCARD,)
|
||||
RETURN_NAMES = ("model",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
|
||||
def run(self, model=""):
|
||||
# Split the model path by '/' and select the last item
|
||||
model_name = model.split('/')[-1]
|
||||
return [model_name]
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"ComfyUIDeployModelList": ComfyUIDeployModalList}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployModelList": "Model List (ComfyUI Deploy)"}
|
||||
@@ -0,0 +1,99 @@
|
||||
import os
|
||||
import json
|
||||
import folder_paths
|
||||
|
||||
|
||||
class ComfyDeployOutputText:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "output"
|
||||
self.prefix_append = ""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"text": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": True,
|
||||
"forceInput": True,
|
||||
"tooltip": "The text to save.",
|
||||
},
|
||||
),
|
||||
"filename_prefix": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "ComfyUI",
|
||||
"tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% to include values from nodes.",
|
||||
},
|
||||
),
|
||||
"file_type": (["txt", "json", "md"], {"default": "txt"}),
|
||||
},
|
||||
"optional": {
|
||||
"output_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "output_text"},
|
||||
),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "run"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
DESCRIPTION = "Saves the input text to your ComfyUI output directory."
|
||||
|
||||
def run(
|
||||
self,
|
||||
text,
|
||||
filename_prefix="ComfyUI",
|
||||
file_type="txt",
|
||||
output_id="output_text",
|
||||
prompt=None,
|
||||
extra_pnginfo=None,
|
||||
):
|
||||
filename_prefix += self.prefix_append
|
||||
# For text, we don't need dimensions, so pass 0, 0
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = (
|
||||
folder_paths.get_save_image_path(filename_prefix, self.output_dir, 0, 0)
|
||||
)
|
||||
|
||||
results = list()
|
||||
|
||||
# Create file path
|
||||
file = f"{filename}_{counter:05}_.{file_type}"
|
||||
file_path = os.path.join(full_output_folder, file)
|
||||
|
||||
# Save the text based on file type
|
||||
if file_type == "json":
|
||||
try:
|
||||
# Try to save as JSON if the text is valid JSON
|
||||
json_data = json.loads(text) if isinstance(text, str) else text
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(json_data, f, indent=2)
|
||||
except json.JSONDecodeError:
|
||||
# Fall back to saving as plain text if not valid JSON
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
else:
|
||||
# Save as plain text for txt and md
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"filename": file,
|
||||
"subfolder": subfolder,
|
||||
"type": self.type,
|
||||
"output_id": output_id,
|
||||
}
|
||||
)
|
||||
|
||||
return {"ui": {"text_file": results}}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"ComfyDeployOutputText": ComfyDeployOutputText}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyDeployOutputText": "Text Output (ComfyDeploy)"}
|
||||
+135
-57
@@ -139,7 +139,9 @@ async def async_request_with_retry(
|
||||
logger.error(f"Error response body: {error_body}")
|
||||
|
||||
if attempt == max_retries - 1:
|
||||
logger.error(f"Request {method} : {url} failed after {max_retries} attempts: {e}")
|
||||
logger.error(
|
||||
f"Request {method} : {url} failed after {max_retries} attempts: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
await asyncio.sleep(retry_delay)
|
||||
@@ -156,6 +158,8 @@ from logging import basicConfig, getLogger
|
||||
# Check for an environment variable to enable/disable Logfire
|
||||
use_logfire = os.environ.get("USE_LOGFIRE", "false").lower() == "true"
|
||||
|
||||
API_KEY_COMFY_ORG = os.environ.get("API_KEY_COMFY_ORG", None)
|
||||
|
||||
if use_logfire:
|
||||
try:
|
||||
import logfire
|
||||
@@ -281,6 +285,9 @@ def post_prompt(json_data):
|
||||
if "extra_data" in json_data:
|
||||
extra_data = json_data["extra_data"]
|
||||
|
||||
if API_KEY_COMFY_ORG is not None:
|
||||
extra_data["api_key_comfy_org"] = API_KEY_COMFY_ORG
|
||||
|
||||
if "client_id" in json_data:
|
||||
extra_data["client_id"] = json_data["client_id"]
|
||||
if valid[0]:
|
||||
@@ -314,7 +321,7 @@ def randomSeed(num_digits=15):
|
||||
return random.randint(range_start, range_end)
|
||||
|
||||
|
||||
def apply_random_seed_to_workflow(workflow_api):
|
||||
def apply_random_seed_to_workflow(workflow_api, workflow):
|
||||
"""
|
||||
Applies a random seed to each element in the workflow_api that has a 'seed' input.
|
||||
|
||||
@@ -327,6 +334,41 @@ def apply_random_seed_to_workflow(workflow_api):
|
||||
# If seed is a list, it's an input from another node (generally `external number int`)
|
||||
if isinstance(workflow_api[key]["inputs"]["seed"], list):
|
||||
continue
|
||||
|
||||
# Check node type in workflow to determine if we should randomize
|
||||
node_id = key
|
||||
should_skip = (
|
||||
False # Add a flag to track if we should skip randomization
|
||||
)
|
||||
|
||||
for node in workflow["nodes"]:
|
||||
if str(node["id"]) == node_id and node["type"] == "KSampler":
|
||||
# Check if this node has widgets_values and if seed setting is not "fixed"
|
||||
if "widgets_values" in node and len(node["widgets_values"]) > 1:
|
||||
seed_mode = node["widgets_values"][1]
|
||||
if seed_mode == "fixed":
|
||||
# Skip randomization for fixed seeds
|
||||
logger.info(
|
||||
f"Skipping random seed for KSampler (node {node_id}) as it's set to fixed"
|
||||
)
|
||||
should_skip = True # Set the flag to skip randomization
|
||||
break # Exit the inner loop
|
||||
|
||||
# Apply random seed for non-fixed seeds (randomize, iter, etc.)
|
||||
workflow_api[key]["inputs"]["seed"] = randomSeed()
|
||||
logger.info(
|
||||
f"Applied random seed {workflow_api[key]['inputs']['seed']} to KSampler (node {node_id})"
|
||||
)
|
||||
should_skip = (
|
||||
True # Set the flag to skip default randomization
|
||||
)
|
||||
break # Exit the inner loop
|
||||
break # This break will skip checking other nodes if widgets_values doesn't exist
|
||||
|
||||
# Skip the rest of the code for this key if we already handled it
|
||||
if should_skip:
|
||||
continue
|
||||
|
||||
# Special case for SONICSampler
|
||||
if workflow_api[key]["class_type"] == "SONICSampler":
|
||||
workflow_api[key]["inputs"]["seed"] = randomSeed("sonic")
|
||||
@@ -367,6 +409,12 @@ def apply_random_seed_to_workflow(workflow_api):
|
||||
f"Applied random noise_seed {workflow_api[key]['inputs']['noise_seed']} to SamplerCustom"
|
||||
)
|
||||
continue
|
||||
if workflow_api[key]["class_type"] == "XlabsSampler":
|
||||
workflow_api[key]["inputs"]["noise_seed"] = randomSeed()
|
||||
logger.info(
|
||||
f"Applied random noise_seed {workflow_api[key]['inputs']['noise_seed']} to SamplerCustom"
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
|
||||
@@ -407,6 +455,9 @@ def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
|
||||
if value["class_type"] == "ComfyUIDeployExternalImageBatch":
|
||||
value["inputs"]["images"] = new_value
|
||||
|
||||
if value["class_type"] == "ComfyUIDeployExternalEnum":
|
||||
value["inputs"]["default_value"] = new_value
|
||||
|
||||
if value["class_type"] == "ComfyUIDeployExternalLora":
|
||||
value["inputs"]["lora_url"] = new_value
|
||||
|
||||
@@ -425,6 +476,12 @@ def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
|
||||
if value["class_type"] == "ComfyUIDeployExternalEXR":
|
||||
value["inputs"]["exr_file"] = new_value
|
||||
|
||||
if value["class_type"] == "ComfyUIDeployExternalSeed":
|
||||
logger.info(
|
||||
f"Applied random seed {new_value} to {value['class_type']}"
|
||||
)
|
||||
value["inputs"]["default_value"] = new_value
|
||||
|
||||
|
||||
def send_prompt(sid: str, inputs: StreamingPrompt):
|
||||
# workflow_api = inputs.workflow_api
|
||||
@@ -432,7 +489,7 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
|
||||
workflow = copy.deepcopy(inputs.workflow)
|
||||
|
||||
# Random seed
|
||||
apply_random_seed_to_workflow(workflow_api)
|
||||
apply_random_seed_to_workflow(workflow_api, workflow)
|
||||
|
||||
logger.info("getting inputs", inputs.inputs)
|
||||
|
||||
@@ -518,7 +575,7 @@ async def comfy_deploy_run(request):
|
||||
workflow = data.get("workflow")
|
||||
|
||||
# Now it handles directly in here
|
||||
apply_random_seed_to_workflow(workflow_api)
|
||||
apply_random_seed_to_workflow(workflow_api, workflow)
|
||||
apply_inputs_to_workflow(workflow_api, inputs)
|
||||
|
||||
prompt = {
|
||||
@@ -585,7 +642,7 @@ async def stream_prompt(data, token):
|
||||
gpu_event_id = data.get("gpu_event_id", None)
|
||||
|
||||
# Now it handles directly in here
|
||||
apply_random_seed_to_workflow(workflow_api)
|
||||
apply_random_seed_to_workflow(workflow_api, workflow)
|
||||
apply_inputs_to_workflow(workflow_api, inputs)
|
||||
|
||||
prompt = {
|
||||
@@ -1304,7 +1361,7 @@ send_json = prompt_server.send_json
|
||||
|
||||
|
||||
async def send_json_override(self, event, data, sid=None):
|
||||
# logger.info("INTERNAL:", event, data, sid)
|
||||
# logger.info(f"INTERNAL: event={event}, data={data}, sid={sid}")
|
||||
prompt_id = data.get("prompt_id")
|
||||
|
||||
target_sid = sid
|
||||
@@ -1389,9 +1446,12 @@ async def send_json_override(self, event, data, sid=None):
|
||||
# the last executing event is none, then the workflow is finished
|
||||
if event == "executing" and data.get("node") is None:
|
||||
mark_prompt_done(prompt_id=prompt_id)
|
||||
# We will now rely on the UploadQueue worker to set the final SUCCESS status
|
||||
# after all uploads are confirmed complete.
|
||||
|
||||
if not have_pending_upload(prompt_id):
|
||||
await update_run(prompt_id, Status.SUCCESS)
|
||||
if prompt_id in prompt_metadata:
|
||||
# await update_run(prompt_id, Status.SUCCESS) # <-- REMOVE/COMMENT OUT
|
||||
if prompt_id in prompt_metadata: # <-- REMOVE/COMMENT OUT THIS BLOCK
|
||||
current_time = time.perf_counter()
|
||||
if prompt_metadata[prompt_id].start_time is not None:
|
||||
elapsed_time = current_time - prompt_metadata[prompt_id].start_time
|
||||
@@ -1832,12 +1892,13 @@ async def upload_file(
|
||||
|
||||
|
||||
def have_pending_upload(prompt_id):
|
||||
# Check if there are pending uploads in the queue
|
||||
if (
|
||||
prompt_id in prompt_metadata
|
||||
and len(prompt_metadata[prompt_id].uploading_nodes) > 0
|
||||
prompt_id in upload_queue.pending_uploads
|
||||
and upload_queue.pending_uploads[prompt_id]
|
||||
):
|
||||
logger.info(
|
||||
f"Have pending upload {len(prompt_metadata[prompt_id].uploading_nodes)}"
|
||||
f"Have pending upload {len(upload_queue.pending_uploads[prompt_id])}"
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -1892,16 +1953,11 @@ async def handle_error(prompt_id, data, e: Exception):
|
||||
async def update_file_status(
|
||||
prompt_id: str, data, uploading, have_error=False, node_id=None
|
||||
):
|
||||
# if 'uploading_nodes' not in prompt_metadata[prompt_id]:
|
||||
# prompt_metadata[prompt_id]['uploading_nodes'] = set()
|
||||
# We're using upload_queue as the single source of truth for tracking uploads
|
||||
# The upload_queue.pending_uploads is managed by the UploadQueue class itself
|
||||
# We no longer need to track uploading_nodes in prompt_metadata
|
||||
|
||||
if node_id is not None:
|
||||
if uploading:
|
||||
prompt_metadata[prompt_id].uploading_nodes.add(node_id)
|
||||
else:
|
||||
prompt_metadata[prompt_id].uploading_nodes.discard(node_id)
|
||||
|
||||
# logger.info(f"Remaining uploads: {prompt_metadata[prompt_id].uploading_nodes}")
|
||||
# logger.info(f"Pending uploads in queue: {upload_queue.pending_uploads.get(prompt_id, set())}")
|
||||
# Update the remote status
|
||||
|
||||
if have_error:
|
||||
@@ -1915,15 +1971,15 @@ async def update_file_status(
|
||||
return
|
||||
|
||||
# if there are still nodes that are uploading, then we set the status to uploading
|
||||
if uploading:
|
||||
if prompt_metadata[prompt_id].status != Status.UPLOADING:
|
||||
await update_run(prompt_id, Status.UPLOADING)
|
||||
await send(
|
||||
"uploading",
|
||||
{
|
||||
"prompt_id": prompt_id,
|
||||
},
|
||||
)
|
||||
# if uploading:
|
||||
# if prompt_metadata[prompt_id].status != Status.UPLOADING:
|
||||
# await update_run(prompt_id, Status.UPLOADING)
|
||||
# await send(
|
||||
# "uploading",
|
||||
# {
|
||||
# "prompt_id": prompt_id,
|
||||
# },
|
||||
# )
|
||||
|
||||
# if there are no nodes that are uploading, then we set the status to success
|
||||
elif (
|
||||
@@ -1949,7 +2005,7 @@ async def handle_upload(
|
||||
|
||||
for item in items:
|
||||
# Skipping temp files
|
||||
if item.get("type") == "temp":
|
||||
if isinstance(item, dict) and item.get("type") == "temp":
|
||||
continue
|
||||
|
||||
file_type = item.get(content_type_key, default_content_type)
|
||||
@@ -2000,22 +2056,29 @@ async def upload_in_background(
|
||||
("files", "content_type", "image/png"),
|
||||
("gifs", "format", "image/gif"),
|
||||
("model_file", "format", "application/octet-stream"),
|
||||
("result", "format", "application/octet-stream"),
|
||||
("text_file", "format", "text/plain"),
|
||||
]:
|
||||
items = data.get(file_type, [])
|
||||
|
||||
for item in items:
|
||||
# if is model_file, just add it to the data
|
||||
if file_type == "model_file":
|
||||
if file_type == "model_file" or file_type == "result":
|
||||
if isinstance(item, str):
|
||||
filename = os.path.basename(item)
|
||||
# Extract folder name from the path
|
||||
folder_path = os.path.dirname(item)
|
||||
subfolder = (
|
||||
os.path.basename(folder_path) if folder_path else ""
|
||||
)
|
||||
item = {
|
||||
"filename": filename,
|
||||
"subfolder": "",
|
||||
"subfolder": subfolder,
|
||||
"type": "output",
|
||||
}
|
||||
|
||||
# Skip temp files
|
||||
if item.get("type") == "temp":
|
||||
if isinstance(item, dict) and item.get("type") == "temp":
|
||||
continue
|
||||
|
||||
# Add to the upload queue instead of uploading immediately
|
||||
@@ -2080,6 +2143,8 @@ async def update_run_with_output(
|
||||
or "files" in data
|
||||
or "gifs" in data
|
||||
or "model_file" in data
|
||||
or "result" in data
|
||||
or "text_file" in data
|
||||
)
|
||||
if bypass_upload and have_upload_media:
|
||||
print(
|
||||
@@ -2390,6 +2455,11 @@ class UploadQueue:
|
||||
logger.warning(f"No upload endpoint for prompt ID: {prompt_id}")
|
||||
return
|
||||
|
||||
# Check if file_info is a valid dictionary with a filename
|
||||
if not isinstance(file_info, dict) or "filename" not in file_info:
|
||||
logger.warning(f"Invalid file_info for prompt ID {prompt_id}: {file_info}")
|
||||
return
|
||||
|
||||
filename = file_info.get("filename")
|
||||
subfolder = file_info.get("subfolder")
|
||||
file_type = file_info.get("type", "output")
|
||||
@@ -2540,8 +2610,12 @@ class UploadQueue:
|
||||
# If this was the last file for this prompt, show the stats summary
|
||||
if (
|
||||
prompt_id in self.pending_uploads
|
||||
# We now rely on the worker's finally block for the final SUCCESS update.
|
||||
# Check if the set becomes empty *after* removal in the worker.
|
||||
and len(self.pending_uploads[prompt_id]) == 1
|
||||
):
|
||||
# await update_run(prompt_id, Status.SUCCESS) # <-- REMOVE/COMMENT OUT
|
||||
|
||||
self._log_upload_stats(prompt_id)
|
||||
# Clean up stats
|
||||
del self.upload_stats[prompt_id]
|
||||
@@ -2662,6 +2736,8 @@ class UploadQueue:
|
||||
node_id = upload_task["node_id"]
|
||||
upload_id = upload_task["upload_id"]
|
||||
|
||||
print(file_info)
|
||||
|
||||
try:
|
||||
# Coordinate the actual start of the upload
|
||||
async with self.upload_lock:
|
||||
@@ -2691,30 +2767,32 @@ class UploadQueue:
|
||||
# If this was the last upload for this node, clean up node data
|
||||
if not self.node_uploads[prompt_id][node_id]:
|
||||
del self.node_uploads[prompt_id][node_id]
|
||||
if self.node_output_data[prompt_id][node_id]["data"]:
|
||||
# Send final node data to API before cleanup
|
||||
if prompt_metadata[prompt_id].status_endpoint:
|
||||
body = {
|
||||
"run_id": prompt_id,
|
||||
"output_data": self.node_output_data[
|
||||
prompt_id
|
||||
][node_id]["data"],
|
||||
"node_meta": {"node_id": node_id},
|
||||
}
|
||||
try:
|
||||
await async_request_with_retry(
|
||||
"POST",
|
||||
prompt_metadata[
|
||||
prompt_id
|
||||
].status_endpoint,
|
||||
token=prompt_metadata[prompt_id].token,
|
||||
json=body,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to send final node data: {str(e)}"
|
||||
)
|
||||
del self.node_output_data[prompt_id][node_id]
|
||||
if prompt_id in self.node_output_data:
|
||||
if node_id in self.node_output_data[prompt_id]:
|
||||
if self.node_output_data[prompt_id][node_id]["data"]:
|
||||
# Send final node data to API before cleanup
|
||||
if prompt_metadata[prompt_id].status_endpoint:
|
||||
body = {
|
||||
"run_id": prompt_id,
|
||||
"output_data": self.node_output_data[
|
||||
prompt_id
|
||||
][node_id]["data"],
|
||||
"node_meta": {"node_id": node_id},
|
||||
}
|
||||
try:
|
||||
await async_request_with_retry(
|
||||
"POST",
|
||||
prompt_metadata[
|
||||
prompt_id
|
||||
].status_endpoint,
|
||||
token=prompt_metadata[prompt_id].token,
|
||||
json=body,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to send final node data: {str(e)}"
|
||||
)
|
||||
del self.node_output_data[prompt_id][node_id]
|
||||
|
||||
# Send status update
|
||||
await self.update_queue_status(prompt_id)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"id": "ed93ac94-4f26-4ed3-a57b-73cd8f4d3494",
|
||||
"revision": 0,
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoraLoader",
|
||||
"pos": [
|
||||
736.646728515625,
|
||||
628.3823852539062
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
126
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "lora_name",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "lora_name"
|
||||
},
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "LoraLoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"1-292.safetensors",
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "ComfyUIDeployExternalLora",
|
||||
"pos": [
|
||||
299.6898498535156,
|
||||
624.7929077148438
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
208
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "path",
|
||||
"type": "*",
|
||||
"links": [
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-deploy",
|
||||
"ver": "cd3a2ff5471828f9c840e746551592e882c05aa4",
|
||||
"Node name for S&R": "ComfyUIDeployExternalLora"
|
||||
},
|
||||
"widgets_values": [
|
||||
"input_lora",
|
||||
"HyperSD\\FLUX.1\\Hyper-FLUX.1-dev-16steps-lora.safetensors",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "Note",
|
||||
"pos": [
|
||||
302.09033203125,
|
||||
401.2951965332031
|
||||
],
|
||||
"size": [
|
||||
479.4894104003906,
|
||||
161.61924743652344
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"\"External Lora\" node will let you to use different loras from the Comfy Deploy UI or even via API.\n\n- lora_url:\n url that will be used to download your LoRA model in execution time\n\n- lora_save_name:\n when we download your model, this will be saved in your private storage, \n give it a good name :D"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
"COMBO"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.167184107045006,
|
||||
"offset": [
|
||||
298.431389807788,
|
||||
-207.58877445762934
|
||||
]
|
||||
},
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
@@ -0,0 +1,873 @@
|
||||
{
|
||||
"id": "351f402b-62f2-4f62-8a5e-0b9d3510e8f9",
|
||||
"revision": 0,
|
||||
"last_node_id": 29,
|
||||
"last_link_id": 28,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 11,
|
||||
"type": "JoinImageWithAlpha",
|
||||
"pos": [
|
||||
814.478271484375,
|
||||
419.3052062988281
|
||||
],
|
||||
"size": [
|
||||
264.5999755859375,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 10
|
||||
},
|
||||
{
|
||||
"name": "alpha",
|
||||
"type": "MASK",
|
||||
"link": 12
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
11
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "JoinImageWithAlpha"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1950,
|
||||
640
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 18,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 16
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
467.8168640136719,
|
||||
422.453857421875
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
12
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Bob-Minion-Background-PNG-Image.png",
|
||||
"image",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "Note",
|
||||
"pos": [
|
||||
460.8001708984375,
|
||||
263.64251708984375
|
||||
],
|
||||
"size": [
|
||||
379.4292297363281,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Option 1: CREATE THE MASK FROM THE ALPHA CHANNEL (Useful for example to generate the background of an image)\n\nMake sure that you are using \"External Image Alpha\". \n"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
469.57025146484375,
|
||||
1506.3018798828125
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
18
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"ComfyUI_temp_otmos_00005_.png",
|
||||
"image",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"type": "MaskToImage",
|
||||
"pos": [
|
||||
1283.1668701171875,
|
||||
1579.603271484375
|
||||
],
|
||||
"size": [
|
||||
176.39999389648438,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 13,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 28
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
21
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "MaskToImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "SplitImageWithAlpha",
|
||||
"pos": [
|
||||
1602.5518798828125,
|
||||
417.59869384765625
|
||||
],
|
||||
"size": [
|
||||
277.20001220703125,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
14,
|
||||
25
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
15,
|
||||
26
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "SplitImageWithAlpha"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"type": "VAEEncodeForInpaint",
|
||||
"pos": [
|
||||
2220.89453125,
|
||||
401.0885009765625
|
||||
],
|
||||
"size": [
|
||||
340.20001220703125,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 16,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pixels",
|
||||
"type": "IMAGE",
|
||||
"link": 25
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 26
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "VAEEncodeForInpaint"
|
||||
},
|
||||
"widgets_values": [
|
||||
6
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1950,
|
||||
350
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 14,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 14
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "MaskToImage",
|
||||
"pos": [
|
||||
1752.33203125,
|
||||
572.061767578125
|
||||
],
|
||||
"size": [
|
||||
176.39999389648438,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 15,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 15
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
16
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "MaskToImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1329.2960205078125,
|
||||
1132.759765625
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 22
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
468.1963806152344,
|
||||
1128.9498291015625
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
17
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"clipspace/clipspace-mask-1126817.300000012.png [input]",
|
||||
"image",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"type": "ImageToMask",
|
||||
"pos": [
|
||||
1255.560302734375,
|
||||
1468.347900390625
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 19
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
24,
|
||||
28
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "ImageToMask"
|
||||
},
|
||||
"widgets_values": [
|
||||
"red"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1488.1925048828125,
|
||||
1579.146728515625
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 17,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 21
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"type": "VAEEncodeForInpaint",
|
||||
"pos": [
|
||||
1637.603271484375,
|
||||
1182.81298828125
|
||||
],
|
||||
"size": [
|
||||
340.20001220703125,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 12,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pixels",
|
||||
"type": "IMAGE",
|
||||
"link": 23
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 24
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.27",
|
||||
"Node name for S&R": "VAEEncodeForInpaint"
|
||||
},
|
||||
"widgets_values": [
|
||||
6
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "Note",
|
||||
"pos": [
|
||||
467.4615173339844,
|
||||
985.1439819335938
|
||||
],
|
||||
"size": [
|
||||
379.4292297363281,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Option 2: UPLOAD THE MASK AS OTHER IMAGE (useful if you require the image that is below the mask to do inpainting, in this case to generate new glasses)"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"type": "ComfyUIDeployExternalImage",
|
||||
"pos": [
|
||||
910.8199462890625,
|
||||
1132.306396484375
|
||||
],
|
||||
"size": [
|
||||
390.5999755859375,
|
||||
154
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "default_value",
|
||||
"shape": 7,
|
||||
"type": "IMAGE",
|
||||
"link": 17
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
22,
|
||||
23
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-deploy",
|
||||
"ver": "cd3a2ff5471828f9c840e746551592e882c05aa4",
|
||||
"Node name for S&R": "ComfyUIDeployExternalImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"input_image",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"type": "ComfyUIDeployExternalImage",
|
||||
"pos": [
|
||||
843.6348876953125,
|
||||
1467.8876953125
|
||||
],
|
||||
"size": [
|
||||
390.5999755859375,
|
||||
154
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "default_value",
|
||||
"shape": 7,
|
||||
"type": "IMAGE",
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
19
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-deploy",
|
||||
"ver": "cd3a2ff5471828f9c840e746551592e882c05aa4",
|
||||
"Node name for S&R": "ComfyUIDeployExternalImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"input_image_mask",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "ComfyUIDeployExternalImageAlpha",
|
||||
"pos": [
|
||||
1107.891357421875,
|
||||
418.2679138183594
|
||||
],
|
||||
"size": [
|
||||
466.1999816894531,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "default_value",
|
||||
"shape": 7,
|
||||
"type": "IMAGE",
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
13
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-deploy",
|
||||
"ver": "cd3a2ff5471828f9c840e746551592e882c05aa4",
|
||||
"Node name for S&R": "ComfyUIDeployExternalImageAlpha"
|
||||
},
|
||||
"widgets_values": [
|
||||
"input_image_alpha",
|
||||
"",
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
10,
|
||||
10,
|
||||
0,
|
||||
11,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
11,
|
||||
11,
|
||||
0,
|
||||
12,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
12,
|
||||
10,
|
||||
1,
|
||||
11,
|
||||
1,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
13,
|
||||
12,
|
||||
0,
|
||||
13,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
14,
|
||||
13,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
15,
|
||||
13,
|
||||
1,
|
||||
17,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
16,
|
||||
17,
|
||||
0,
|
||||
15,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
17,
|
||||
20,
|
||||
0,
|
||||
22,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
18,
|
||||
21,
|
||||
0,
|
||||
23,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
19,
|
||||
23,
|
||||
0,
|
||||
24,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
21,
|
||||
25,
|
||||
0,
|
||||
26,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
22,
|
||||
22,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
23,
|
||||
22,
|
||||
0,
|
||||
28,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
24,
|
||||
24,
|
||||
0,
|
||||
28,
|
||||
2,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
25,
|
||||
13,
|
||||
0,
|
||||
29,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
26,
|
||||
13,
|
||||
1,
|
||||
29,
|
||||
2,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
28,
|
||||
24,
|
||||
0,
|
||||
25,
|
||||
0,
|
||||
"MASK"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.9646149645000013,
|
||||
"offset": [
|
||||
48.66905973637718,
|
||||
-817.5683540167485
|
||||
]
|
||||
},
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
+16
@@ -56,11 +56,27 @@ streaming_prompt_metadata: dict[str, StreamingPrompt] = {}
|
||||
class BinaryEventTypes:
|
||||
PREVIEW_IMAGE = 1
|
||||
UNENCODED_PREVIEW_IMAGE = 2
|
||||
EXR_IMAGE = 4
|
||||
|
||||
|
||||
max_output_id_length = 24
|
||||
|
||||
|
||||
async def send_exr(image_data, sid=None, output_id: str = None):
|
||||
max_length = max_output_id_length
|
||||
output_id = output_id[:max_length]
|
||||
padded_output_id = output_id.ljust(max_length, "\x00")
|
||||
encoded_output_id = padded_output_id.encode("ascii", "replace")
|
||||
|
||||
bytesIO = BytesIO()
|
||||
# 10 bytes for the output_id
|
||||
bytesIO.write(encoded_output_id)
|
||||
bytesIO.write(image_data)
|
||||
|
||||
preview_bytes = bytesIO.getvalue()
|
||||
await send_bytes(BinaryEventTypes.EXR_IMAGE, preview_bytes, sid=sid)
|
||||
|
||||
|
||||
async def send_image(image_data, sid=None, output_id: str = None):
|
||||
max_length = max_output_id_length
|
||||
output_id = output_id[:max_length]
|
||||
|
||||
+257
-213
@@ -5,7 +5,7 @@ LGraphNode = LiteGraph.LGraphNode;
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
|
||||
import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25";
|
||||
import { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69";
|
||||
// import { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69";
|
||||
|
||||
const styles = `
|
||||
.comfydeploy-menu-item {
|
||||
@@ -168,12 +168,12 @@ function setSelectedWorkflowInfo(info) {
|
||||
|
||||
const VALID_TYPES = [
|
||||
"STRING",
|
||||
"combo",
|
||||
"number",
|
||||
"toggle",
|
||||
"BOOLEAN",
|
||||
"text",
|
||||
"string",
|
||||
"combo",
|
||||
];
|
||||
|
||||
function hideWidget(node, widget, suffix = "") {
|
||||
@@ -181,7 +181,9 @@ function hideWidget(node, widget, suffix = "") {
|
||||
widget.origType = widget.type;
|
||||
widget.origComputeSize = widget.computeSize;
|
||||
widget.origSerializeValue = widget.serializeValue;
|
||||
widget.computeSize = () => [0, -4];
|
||||
// console.log(widget.origComputeSize);
|
||||
// console.log(LiteGraph.NODE_SLOT_HEIGHT);
|
||||
// widget.computeSize = () => [0, 0];
|
||||
widget.type = CONVERTED_TYPE + suffix;
|
||||
widget.serializeValue = () => {
|
||||
if (!node.inputs) {
|
||||
@@ -210,10 +212,37 @@ function getWidgetType(config) {
|
||||
return { type };
|
||||
}
|
||||
|
||||
const GET_CONFIG = Symbol();
|
||||
async function convertToInput(node, widget, config) {
|
||||
const { type } = getWidgetType(config);
|
||||
|
||||
console.log(node, widget, config);
|
||||
|
||||
const result = await app.extensionManager.dialog.prompt(
|
||||
{
|
||||
title: "Convert " + widget.name + " to external input",
|
||||
message: "Input name",
|
||||
defaultValue: widget.name,
|
||||
}
|
||||
);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
// Check for duplicate input IDs across existing external input nodes
|
||||
const existingInputIds = Object.values(app.graph.nodes)
|
||||
.filter(n => n.type.startsWith("ComfyUIDeployExternal"))
|
||||
.map(n => n.widgets_values?.[0])
|
||||
.filter(Boolean);
|
||||
|
||||
if (existingInputIds.includes(result)) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Input ID already exists',
|
||||
detail: 'Please choose a different name.',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
function convertToInput(node, widget, config) {
|
||||
console.log(node);
|
||||
if (node.type == "LoadImage") {
|
||||
var inputNode = LiteGraph.createNode("ComfyUIDeployExternalImage");
|
||||
console.log(widget);
|
||||
@@ -235,54 +264,93 @@ function convertToInput(node, widget, config) {
|
||||
|
||||
const links = app.graph.links;
|
||||
|
||||
console.log(currentOutputsLinks);
|
||||
// console.log(currentOutputsLinks);
|
||||
|
||||
for (let i = 0; i < currentOutputsLinks.length; i++) {
|
||||
const link = currentOutputsLinks[i];
|
||||
const llink = links[link];
|
||||
console.log(links[link]);
|
||||
setTimeout(
|
||||
() => inputNode.connect(0, llink.target_id, llink.target_slot),
|
||||
100,
|
||||
);
|
||||
}
|
||||
if (currentOutputsLinks)
|
||||
for (let i = 0; i < currentOutputsLinks.length; i++) {
|
||||
const link = currentOutputsLinks[i];
|
||||
const llink = links[link];
|
||||
console.log(links[link]);
|
||||
setTimeout(
|
||||
() => inputNode.connect(0, llink.target_id, llink.target_slot),
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
node.connect(0, inputNode, 0);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
hideWidget(node, widget);
|
||||
const { type } = getWidgetType(config);
|
||||
const sz = node.size;
|
||||
const inputIsOptional = !!widget.options?.inputIsOptional;
|
||||
const input = node.addInput(widget.name, type, {
|
||||
widget: { name: widget.name, [GET_CONFIG]: () => config },
|
||||
...(inputIsOptional ? { shape: LiteGraph.SlotShape.HollowCircle } : {}),
|
||||
});
|
||||
for (const widget2 of node.widgets) {
|
||||
widget2.last_y += LiteGraph.NODE_SLOT_HEIGHT;
|
||||
}
|
||||
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
|
||||
let externalNode = "";
|
||||
let inputId = result;
|
||||
|
||||
if (type == "STRING") {
|
||||
var inputNode = LiteGraph.createNode("ComfyUIDeployExternalText");
|
||||
console.log(widget);
|
||||
const index = node.inputs.findIndex((x) => x.name == widget.name);
|
||||
console.log(node.widgets_values, index);
|
||||
if (type === "INT") {
|
||||
externalNode = "ComfyUIDeployExternalNumberInt";
|
||||
// inputId = "input_number";
|
||||
}
|
||||
|
||||
if (type === "FLOAT") {
|
||||
externalNode = "ComfyUIDeployExternalNumberSlider";
|
||||
// inputId = "input_number";
|
||||
}
|
||||
|
||||
if (type === "STRING") {
|
||||
externalNode = "ComfyUIDeployExternalText";
|
||||
// inputId = "input_text";
|
||||
}
|
||||
|
||||
if (type === "COMBO") {
|
||||
externalNode = "ComfyUIDeployExternalEnum";
|
||||
// inputId = "input_enum";
|
||||
}
|
||||
|
||||
if (!externalNode || !inputId) return;
|
||||
|
||||
node.convertWidgetToInput(widget);
|
||||
|
||||
var inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId);
|
||||
|
||||
// if (type === "COMBO") {
|
||||
// inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId, {
|
||||
// dynamic_enum_options: config[0],
|
||||
// });
|
||||
// console.log(inputNode);
|
||||
// const options = config[0];
|
||||
// console.log(options);
|
||||
// } else {
|
||||
// inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId);
|
||||
// }
|
||||
|
||||
var options;
|
||||
|
||||
const index = node.inputs.findIndex((x) => x.name == widget.name);
|
||||
if (type === "COMBO") {
|
||||
options = widget.options?.values ?? config[0];
|
||||
inputNode.configure({
|
||||
widgets_values: ["input_text", widget.value],
|
||||
widgets_values: [inputId, widget.value, JSON.stringify(options)],
|
||||
});
|
||||
inputNode.id = ++app.graph.last_node_id;
|
||||
inputNode.pos = node.pos;
|
||||
inputNode.pos[0] -= node.size[0] + 40;
|
||||
} else {
|
||||
inputNode.configure({
|
||||
widgets_values: [inputId, widget.value],
|
||||
});
|
||||
}
|
||||
inputNode.id = ++app.graph.last_node_id;
|
||||
inputNode.pos = node.pos;
|
||||
inputNode.pos[0] -= node.size[0] + 160;
|
||||
|
||||
if (type === "COMBO") {
|
||||
console.log(inputNode);
|
||||
console.log(app.graph);
|
||||
app.graph.add(inputNode);
|
||||
inputNode.connect(0, node, index);
|
||||
console.log(options);
|
||||
inputNode.widgets.find((x) => x.name == "default_value").options.values = options;
|
||||
}
|
||||
|
||||
return input;
|
||||
app.graph.add(inputNode);
|
||||
inputNode.connect(0, node, index);
|
||||
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
|
||||
return node.inputs.find((x) => x.name == widget.name);
|
||||
}
|
||||
|
||||
const CONVERTED_TYPE = "converted-widget";
|
||||
@@ -295,7 +363,12 @@ function getConfig(widgetName) {
|
||||
);
|
||||
}
|
||||
|
||||
function isConvertibleWidget(widget, config) {
|
||||
function isConvertibleWidget(node, widget, config) {
|
||||
// console.log(config);
|
||||
if (node.type === "LoadImage" && widget.type === "combo" && widget.name == "image") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
(VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) &&
|
||||
!widget.options?.forceInput
|
||||
@@ -442,11 +515,11 @@ const ext = {
|
||||
w.type,
|
||||
w.options || {},
|
||||
];
|
||||
if (isConvertibleWidget(w, config)) {
|
||||
if (isConvertibleWidget(this, w, config)) {
|
||||
toInput.push({
|
||||
content: `Convert ${w.name} to external input`,
|
||||
callback: /* @__PURE__ */ __name(
|
||||
() => convertToInput(this, w, config),
|
||||
async () => convertToInput(this, w, config),
|
||||
"callback",
|
||||
),
|
||||
className: "comfydeploy-menu-item",
|
||||
@@ -501,6 +574,13 @@ const ext = {
|
||||
console.log(nodeData.input.optional.default_value_url);
|
||||
}
|
||||
|
||||
if (
|
||||
nodeData?.input?.optional?.default_value?.[1]?.dynamic_enum === true
|
||||
) {
|
||||
nodeData.input.optional.default_value = ["DYNAMIC_ENUM"];
|
||||
// console.log(nodeData.input.optional.default_value);
|
||||
}
|
||||
|
||||
// const origonNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
// nodeType.prototype.onNodeCreated = function () {
|
||||
// const r = origonNodeCreated
|
||||
@@ -535,6 +615,10 @@ const ext = {
|
||||
// };
|
||||
},
|
||||
|
||||
// async nodeCreated(node) {
|
||||
|
||||
// },
|
||||
|
||||
registerCustomNodes() {
|
||||
/** @type {LGraphNode}*/
|
||||
class ComfyDeploy extends LGraphNode {
|
||||
@@ -696,9 +780,37 @@ const ext = {
|
||||
|
||||
return { widget: urlWidget };
|
||||
},
|
||||
|
||||
DYNAMIC_ENUM(node, inputName, inputData) {
|
||||
// console.log("DYNAMIC_ENUM", JSON.parse(JSON.stringify(node)), inputName, inputData);
|
||||
const enumWidget = node.addWidget(
|
||||
"combo",
|
||||
inputName,
|
||||
"",
|
||||
{ serialize: true, values: [] },
|
||||
);
|
||||
|
||||
return { widget: enumWidget };
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async afterConfigureGraph() {
|
||||
app.graph.nodes.forEach(node => {
|
||||
if (node.type === "ComfyUIDeployExternalEnum") {
|
||||
const default_value_index = node.widgets.findIndex(x => x.name === "default_value");
|
||||
const options_index = node.widgets.findIndex(x => x.name === "options");
|
||||
|
||||
var dynamic_enum_options = [node.widgets[default_value_index].value];
|
||||
if (node.widgets[options_index].value) {
|
||||
dynamic_enum_options = JSON.parse(node.widgets[options_index].value);
|
||||
}
|
||||
// console.log("dynamic_enum_options", dynamic_enum_options);
|
||||
node.widgets[default_value_index].options.values = dynamic_enum_options;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async setup() {
|
||||
// const graphCanvas = document.getElementById("graph-canvas");
|
||||
|
||||
@@ -886,6 +998,7 @@ const ext = {
|
||||
}
|
||||
})(app.graph.onAfterChange);
|
||||
|
||||
|
||||
sendEventToCD("cd_plugin_setup");
|
||||
},
|
||||
};
|
||||
@@ -1071,6 +1184,8 @@ async function deployWorkflow() {
|
||||
const prompt = await app.graphToPrompt();
|
||||
let deps = undefined;
|
||||
|
||||
console.log(prompt);
|
||||
|
||||
if (includeDeps) {
|
||||
loadingDialog.showLoading("Fetching existing version");
|
||||
|
||||
@@ -1280,85 +1395,6 @@ async function deployWorkflow() {
|
||||
}
|
||||
}
|
||||
|
||||
// Add this function to refresh the workflows list
|
||||
function refreshWorkflowsList(el) {
|
||||
const workflowsList = el.querySelector("#workflows-list");
|
||||
const workflowsLoading = el.querySelector("#workflows-loading");
|
||||
|
||||
workflowsLoading.style.display = "flex";
|
||||
workflowsList.style.display = "none";
|
||||
workflowsList.innerHTML = "";
|
||||
|
||||
client.workflows
|
||||
.getAll({
|
||||
page: "1",
|
||||
pageSize: "10",
|
||||
})
|
||||
.then((result) => {
|
||||
workflowsLoading.style.display = "none";
|
||||
workflowsList.style.display = "block";
|
||||
|
||||
if (result.length === 0) {
|
||||
workflowsList.innerHTML =
|
||||
"<li style='color: #bdbdbd;'>No workflows found</li>";
|
||||
return;
|
||||
}
|
||||
|
||||
result.forEach((workflow) => {
|
||||
const li = document.createElement("li");
|
||||
li.style.marginBottom = "15px";
|
||||
li.style.padding = "15px";
|
||||
li.style.backgroundColor = "#2a2a2a";
|
||||
li.style.borderRadius = "8px";
|
||||
li.style.boxShadow = "0 2px 4px rgba(0,0,0,0.1)";
|
||||
|
||||
const lastRun = workflow.runs[0];
|
||||
const lastRunStatus = lastRun ? lastRun.status : "No runs";
|
||||
const statusColor =
|
||||
lastRunStatus === "success"
|
||||
? "#4CAF50"
|
||||
: lastRunStatus === "error"
|
||||
? "#F44336"
|
||||
: "#FFC107";
|
||||
|
||||
const timeAgo = getTimeAgo(new Date(workflow.updatedAt));
|
||||
|
||||
li.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<div style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
<strong style="font-size: 18px; color: #e0e0e0;">${workflow.name}</strong>
|
||||
</div>
|
||||
<span style="font-size: 12px; color: ${statusColor}; margin-left: 10px;">Last run: ${lastRunStatus}</span>
|
||||
</div>
|
||||
<div style="font-size: 14px; color: #bdbdbd; margin-bottom: 10px;">Last updated ${timeAgo}</div>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button class="open-cloud-btn" style="padding: 5px 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Open in Cloud</button>
|
||||
<button class="load-api-btn" style="padding: 5px 10px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">Load Workflow</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const openCloudBtn = li.querySelector(".open-cloud-btn");
|
||||
openCloudBtn.onclick = () =>
|
||||
window.open(
|
||||
`${getData().endpoint}/workflows/${workflow.id}?workspace=true`,
|
||||
"_blank",
|
||||
);
|
||||
|
||||
const loadApiBtn = li.querySelector(".load-api-btn");
|
||||
loadApiBtn.onclick = () => loadWorkflowApi(workflow.versions[0].id);
|
||||
|
||||
workflowsList.appendChild(li);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching workflows:", error);
|
||||
workflowsLoading.style.display = "none";
|
||||
workflowsList.style.display = "block";
|
||||
workflowsList.innerHTML =
|
||||
"<li style='color: #F44336;'>Error fetching workflows</li>";
|
||||
});
|
||||
}
|
||||
|
||||
function addButton() {
|
||||
const menu = document.querySelector(".comfy-menu");
|
||||
|
||||
@@ -1845,85 +1881,93 @@ export class ConfigDialog extends ComfyDialog {
|
||||
export const configDialog = new ConfigDialog();
|
||||
|
||||
const currentOrigin = window.location.origin;
|
||||
const client = new ComfyDeploy({
|
||||
bearerAuth: getData().apiKey,
|
||||
serverURL: `${currentOrigin}/comfydeploy/api/`,
|
||||
});
|
||||
// const client = new ComfyDeploy({
|
||||
// bearerAuth: getData().apiKey,
|
||||
// serverURL: `${currentOrigin}/comfydeploy/api/`,
|
||||
// });
|
||||
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "search",
|
||||
icon: "pi pi-cloud-upload",
|
||||
title: "Deploy",
|
||||
tooltip: "Deploy and Configure",
|
||||
type: "custom",
|
||||
render: (el) => {
|
||||
el.innerHTML = `
|
||||
<div style="padding: 20px;">
|
||||
<h3>Comfy Deploy</h3>
|
||||
<div id="deploy-container" style="margin-bottom: 20px;"></div>
|
||||
<div id="workflows-container" style="display: none;">
|
||||
<h4>Your Workflows</h4>
|
||||
<div id="workflows-loading" style="display: flex; justify-content: center; align-items: center; height: 100px;">
|
||||
${loadingIcon}
|
||||
// Check if the current URL hostname starts with localhost or 127.0.0.1
|
||||
const isLocalhost =
|
||||
window.location.hostname === "localhost" ||
|
||||
window.location.hostname === "127.0.0.1";
|
||||
|
||||
// Only register the sidebar tab if we're on localhost
|
||||
if (isLocalhost) {
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "search",
|
||||
icon: "pi pi-cloud-upload",
|
||||
title: "Deploy",
|
||||
tooltip: "Deploy and Configure",
|
||||
type: "custom",
|
||||
render: (el) => {
|
||||
el.innerHTML = `
|
||||
<div style="padding: 20px;">
|
||||
<h3>Comfy Deploy</h3>
|
||||
<div id="deploy-container" style="margin-bottom: 20px;"></div>
|
||||
<div id="workflows-container" style="display: none;">
|
||||
<h4>Your Workflows</h4>
|
||||
<div id="workflows-loading" style="display: flex; justify-content: center; align-items: center; height: 100px;">
|
||||
${loadingIcon}
|
||||
</div>
|
||||
<ul id="workflows-list" style="list-style-type: none; padding: 0; display: none;"></ul>
|
||||
</div>
|
||||
<ul id="workflows-list" style="list-style-type: none; padding: 0; display: none;"></ul>
|
||||
<div id="config-container"></div>
|
||||
</div>
|
||||
<div id="config-container"></div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
|
||||
// Add deploy button
|
||||
const deployContainer = el.querySelector("#deploy-container");
|
||||
const deployButton = document.createElement("button");
|
||||
deployButton.id = "sidebar-deploy-button";
|
||||
deployButton.style.display = "flex";
|
||||
deployButton.style.alignItems = "center";
|
||||
deployButton.style.justifyContent = "center";
|
||||
deployButton.style.width = "100%";
|
||||
deployButton.style.marginBottom = "10px";
|
||||
deployButton.style.padding = "10px";
|
||||
deployButton.style.fontSize = "16px";
|
||||
deployButton.style.fontWeight = "bold";
|
||||
deployButton.style.backgroundColor = "#4CAF50";
|
||||
deployButton.style.color = "white";
|
||||
deployButton.style.border = "none";
|
||||
deployButton.style.borderRadius = "5px";
|
||||
deployButton.style.cursor = "pointer";
|
||||
deployButton.innerHTML = `<i class="pi pi-cloud-upload" style="margin-right: 8px;"></i><div id='sidebar-button-title'>Deploy</div>`;
|
||||
deployButton.onclick = async () => {
|
||||
await deployWorkflow();
|
||||
// Refresh the workflows list after deployment
|
||||
refreshWorkflowsList(el);
|
||||
};
|
||||
deployContainer.appendChild(deployButton);
|
||||
// Add deploy button
|
||||
const deployContainer = el.querySelector("#deploy-container");
|
||||
const deployButton = document.createElement("button");
|
||||
deployButton.id = "sidebar-deploy-button";
|
||||
deployButton.style.display = "flex";
|
||||
deployButton.style.alignItems = "center";
|
||||
deployButton.style.justifyContent = "center";
|
||||
deployButton.style.width = "100%";
|
||||
deployButton.style.marginBottom = "10px";
|
||||
deployButton.style.padding = "10px";
|
||||
deployButton.style.fontSize = "16px";
|
||||
deployButton.style.fontWeight = "bold";
|
||||
deployButton.style.backgroundColor = "#4CAF50";
|
||||
deployButton.style.color = "white";
|
||||
deployButton.style.border = "none";
|
||||
deployButton.style.borderRadius = "5px";
|
||||
deployButton.style.cursor = "pointer";
|
||||
deployButton.innerHTML = `<i class="pi pi-cloud-upload" style="margin-right: 8px;"></i><div id='sidebar-button-title'>Deploy</div>`;
|
||||
deployButton.onclick = async () => {
|
||||
await deployWorkflow();
|
||||
// Refresh the workflows list after deployment
|
||||
// refreshWorkflowsList(el);
|
||||
};
|
||||
deployContainer.appendChild(deployButton);
|
||||
|
||||
// Add config button
|
||||
const configContainer = el.querySelector("#config-container");
|
||||
const configButton = document.createElement("button");
|
||||
configButton.style.display = "flex";
|
||||
configButton.style.alignItems = "center";
|
||||
configButton.style.justifyContent = "center";
|
||||
configButton.style.width = "100%";
|
||||
configButton.style.padding = "8px";
|
||||
configButton.style.fontSize = "14px";
|
||||
configButton.style.backgroundColor = "#f0f0f0";
|
||||
configButton.style.color = "#333";
|
||||
configButton.style.border = "1px solid #ccc";
|
||||
configButton.style.borderRadius = "5px";
|
||||
configButton.style.cursor = "pointer";
|
||||
configButton.innerHTML = `<i class="pi pi-cog" style="margin-right: 8px;"></i>Configure`;
|
||||
configButton.onclick = () => {
|
||||
configDialog.show();
|
||||
};
|
||||
deployContainer.appendChild(configButton);
|
||||
// Add config button
|
||||
const configContainer = el.querySelector("#config-container");
|
||||
const configButton = document.createElement("button");
|
||||
configButton.style.display = "flex";
|
||||
configButton.style.alignItems = "center";
|
||||
configButton.style.justifyContent = "center";
|
||||
configButton.style.width = "100%";
|
||||
configButton.style.padding = "8px";
|
||||
configButton.style.fontSize = "14px";
|
||||
configButton.style.backgroundColor = "#f0f0f0";
|
||||
configButton.style.color = "#333";
|
||||
configButton.style.border = "1px solid #ccc";
|
||||
configButton.style.borderRadius = "5px";
|
||||
configButton.style.cursor = "pointer";
|
||||
configButton.innerHTML = `<i class="pi pi-cog" style="margin-right: 8px;"></i>Configure`;
|
||||
configButton.onclick = () => {
|
||||
configDialog.show();
|
||||
};
|
||||
deployContainer.appendChild(configButton);
|
||||
|
||||
// Fetch and display workflows
|
||||
const workflowsList = el.querySelector("#workflows-list");
|
||||
const workflowsLoading = el.querySelector("#workflows-loading");
|
||||
// Fetch and display workflows
|
||||
const workflowsList = el.querySelector("#workflows-list");
|
||||
const workflowsLoading = el.querySelector("#workflows-loading");
|
||||
|
||||
refreshWorkflowsList(el);
|
||||
},
|
||||
});
|
||||
// refreshWorkflowsList(el);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeAgo(date) {
|
||||
const seconds = Math.floor((new Date() - date) / 1000);
|
||||
@@ -1940,24 +1984,24 @@ function getTimeAgo(date) {
|
||||
return Math.floor(seconds) + " seconds ago";
|
||||
}
|
||||
|
||||
async function loadWorkflowApi(versionId) {
|
||||
try {
|
||||
const response = await client.comfyui.getWorkflowVersionVersionId({
|
||||
versionId: versionId,
|
||||
});
|
||||
// Implement the logic to load the workflow API into the ComfyUI interface
|
||||
console.log("Workflow API loaded:", response);
|
||||
await window["app"].ui.settings.setSettingValueAsync(
|
||||
"Comfy.Validation.Workflows",
|
||||
true,
|
||||
);
|
||||
app.loadGraphData(response.workflow);
|
||||
// You might want to update the UI or trigger some action in ComfyUI here
|
||||
} catch (error) {
|
||||
console.error("Error loading workflow API:", error);
|
||||
// Show an error message to the user
|
||||
}
|
||||
}
|
||||
// async function loadWorkflowApi(versionId) {
|
||||
// try {
|
||||
// const response = await client.comfyui.getWorkflowVersionVersionId({
|
||||
// versionId: versionId,
|
||||
// });
|
||||
// // Implement the logic to load the workflow API into the ComfyUI interface
|
||||
// console.log("Workflow API loaded:", response);
|
||||
// await window["app"].ui.settings.setSettingValueAsync(
|
||||
// "Comfy.Validation.Workflows",
|
||||
// true,
|
||||
// );
|
||||
// app.loadGraphData(response.workflow);
|
||||
// // You might want to update the UI or trigger some action in ComfyUI here
|
||||
// } catch (error) {
|
||||
// console.error("Error loading workflow API:", error);
|
||||
// // Show an error message to the user
|
||||
// }
|
||||
// }
|
||||
|
||||
const orginal_fetch_api = api.fetchApi;
|
||||
api.fetchApi = async (route, options) => {
|
||||
|
||||
Reference in New Issue
Block a user