Compare commits

..
Author SHA1 Message Date
BennyKok 8c8f2abc16 Merge branch 'main' into dev 2024-07-07 22:06:54 -07:00
nick c4d1b09a24 custom route 2024-06-19 16:52:17 -07:00
bennykok c70e08a706 chore(plugin): add log 2024-06-11 17:43:03 -07:00
bennykok daf1669e70 fix: node_error proxy 2024-06-11 17:43:02 -07:00
bennykok 62df715655 fix: prompt error 2024-06-11 17:43:02 -07:00
bennykok 04fd08d5ba fix: streaming event format 2024-06-11 17:43:02 -07:00
bennykok 4a8ef7c77c fix(plugin): event 2024-06-11 17:43:02 -07:00
bennykok 5b8dac37fb feat(plugin): add dispatchAPIEventData 2024-06-11 17:43:02 -07:00
bennykok 875f7f24d1 fix: run issues 2024-06-11 17:43:02 -07:00
bennykok af0fac7afc feat: add streaming endpoint 2024-06-11 17:43:02 -07:00
25 changed files with 580 additions and 2393 deletions
-46
View File
@@ -1,46 +0,0 @@
import json
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
any = AnyType("*")
class ComfyDeployStdOutputAny:
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
return {
"required": {
"name": ("STRING", {"default": "ComfyUI"}),
"source": (any, {}), # Use "*" to accept any input type
},
}
CATEGORY = "output"
RETURN_TYPES = ()
FUNCTION = "run"
OUTPUT_NODE = True
def run(self, name, source=None):
value = "None"
if source is not None:
try:
value = json.dumps(source)
except Exception:
try:
value = str(source)
except Exception:
value = "source exists, but could not be serialized."
return {"ui": {name: (value,)}}
NODE_CLASS_MAPPINGS = {"ComfyDeployStdOutputAny": ComfyDeployStdOutputAny}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyDeployStdOutputAny": "Standard Any Output (ComfyDeploy)"
}
-92
View File
@@ -1,92 +0,0 @@
import os
import json
import numpy as np
from PIL import Image
from PIL.PngImagePlugin import PngInfo
import folder_paths
class ComfyDeployStdOutputImage:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.prefix_append = ""
self.compress_level = 4
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", {"tooltip": "The images 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% or %Empty Latent Image.width% to include values from nodes.",
},
),
"file_type": (["png", "jpg", "webp"], {"default": "webp"}),
"quality": ("INT", {"default": 80, "min": 1, "max": 100, "step": 1}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
FUNCTION = "run"
OUTPUT_NODE = True
CATEGORY = "output"
DESCRIPTION = "Saves the input images to your ComfyUI output directory."
def run(
self,
images,
filename_prefix="ComfyUI",
file_type="png",
quality=80,
prompt=None,
extra_pnginfo=None,
):
filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = (
folder_paths.get_save_image_path(
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]
)
)
results = list()
for batch_number, image in enumerate(images):
i = 255.0 * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.{file_type}"
file_path = os.path.join(full_output_folder, file)
if file_type == "png":
img.save(
file_path, pnginfo=metadata, compress_level=self.compress_level
)
elif file_type == "jpg":
img.save(file_path, quality=quality, optimize=True)
elif file_type == "webp":
img.save(file_path, quality=quality)
results.append(
{"filename": file, "subfolder": subfolder, "type": self.type}
)
counter += 1
return {"ui": {"images": results}}
NODE_CLASS_MAPPINGS = {"ComfyDeployStdOutputImage": ComfyDeployStdOutputImage}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyDeployStdOutputImage": "Standard Image Output (ComfyDeploy)"
}
+1 -11
View File
@@ -8,16 +8,6 @@ class ComfyUIDeployExternalBoolean:
{"multiline": False, "default": "input_bool"}, {"multiline": False, "default": "input_bool"},
), ),
"default_value": ("BOOLEAN", {"default": False}) "default_value": ("BOOLEAN", {"default": False})
},
"optional": {
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -26,7 +16,7 @@ class ComfyUIDeployExternalBoolean:
FUNCTION = "run" FUNCTION = "run"
def run(self, input_id, default_value=None, display_name=None, description=None): def run(self, input_id, default_value=None):
print(f"Node '{input_id}' processing with switch set to {default_value}") print(f"Node '{input_id}' processing with switch set to {default_value}")
return [default_value] return [default_value]
+2 -16
View File
@@ -5,12 +5,6 @@ import torch
import folder_paths import folder_paths
from tqdm import tqdm from tqdm import tqdm
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyUIDeployExternalCheckpoint: class ComfyUIDeployExternalCheckpoint:
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
@@ -23,25 +17,17 @@ class ComfyUIDeployExternalCheckpoint:
}, },
"optional": { "optional": {
"default_value": (folder_paths.get_filename_list("checkpoints"), ), "default_value": (folder_paths.get_filename_list("checkpoints"), ),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
RETURN_TYPES = (WILDCARD,) RETURN_TYPES = (folder_paths.get_filename_list("checkpoints"),)
RETURN_NAMES = ("path",) RETURN_NAMES = ("path",)
FUNCTION = "run" FUNCTION = "run"
CATEGORY = "deploy" CATEGORY = "deploy"
def run(self, input_id, default_value=None, display_name=None, description=None): def run(self, input_id, default_value=None):
import requests import requests
import os import os
import uuid import uuid
-108
View File
@@ -1,108 +0,0 @@
from PIL import Image, ImageOps
import numpy as np
import torch
import folder_paths
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyUIDeployExternalFaceModel:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_reactor_face_model"},
),
},
"optional": {
"default_face_model_name": (
"STRING",
{"multiline": False, "default": ""},
),
"face_model_save_name": ( # if `default_face_model_name` is a link to download a file, we will attempt to save it with this name
"STRING",
{"multiline": False, "default": ""},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
"face_model_url": (
"STRING",
{"multiline": False, "default": ""},
),
},
}
RETURN_TYPES = (WILDCARD,)
RETURN_NAMES = ("path",)
FUNCTION = "run"
CATEGORY = "deploy"
def run(
self,
input_id,
default_face_model_name=None,
face_model_save_name=None,
display_name=None,
description=None,
face_model_url=None,
):
import requests
import os
import uuid
if face_model_url and face_model_url.startswith("http"):
if face_model_save_name:
existing_face_models = folder_paths.get_filename_list("reactor/faces")
# Check if face_model_save_name exists in the list
if face_model_save_name in existing_face_models:
print(f"using face model: {face_model_save_name}")
return (face_model_save_name,)
else:
face_model_save_name = str(uuid.uuid4()) + ".safetensors"
print(face_model_save_name)
print(folder_paths.folder_names_and_paths["reactor/faces"][0][0])
destination_path = os.path.join(
folder_paths.folder_names_and_paths["reactor/faces"][0][0],
face_model_save_name,
)
print(destination_path)
print(
"Downloading external face model - "
+ face_model_url
+ " to "
+ destination_path
)
response = requests.get(
face_model_url,
headers={"User-Agent": "Mozilla/5.0"},
allow_redirects=True,
)
with open(destination_path, "wb") as out_file:
out_file.write(response.content)
return (face_model_save_name,)
else:
print(f"using face model: {default_face_model_name}")
return (default_face_model_name,)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalFaceModel": ComfyUIDeployExternalFaceModel}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyUIDeployExternalFaceModel": "External Face Model (ComfyUI Deploy)"
}
+1 -9
View File
@@ -15,14 +15,6 @@ class ComfyUIDeployExternalImage:
}, },
"optional": { "optional": {
"default_value": ("IMAGE",), "default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -33,7 +25,7 @@ class ComfyUIDeployExternalImage:
CATEGORY = "image" CATEGORY = "image"
def run(self, input_id, default_value=None, display_name=None, description=None): def run(self, input_id, default_value=None):
image = default_value image = default_value
try: try:
if input_id.startswith('http'): if input_id.startswith('http'):
+1 -9
View File
@@ -15,14 +15,6 @@ class ComfyUIDeployExternalImageAlpha:
}, },
"optional": { "optional": {
"default_value": ("IMAGE",), "default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -33,7 +25,7 @@ class ComfyUIDeployExternalImageAlpha:
CATEGORY = "image" CATEGORY = "image"
def run(self, input_id, default_value=None, display_name=None, description=None): def run(self, input_id, default_value=None):
image = default_value image = default_value
try: try:
if input_id.startswith('http'): if input_id.startswith('http'):
+3 -31
View File
@@ -21,14 +21,6 @@ class ComfyUIDeployExternalImageBatch:
}, },
"optional": { "optional": {
"default_value": ("IMAGE",), "default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -39,34 +31,14 @@ class ComfyUIDeployExternalImageBatch:
CATEGORY = "image" CATEGORY = "image"
def process_image(self, image): def run(self, input_id, images=None, default_value=None):
image = ImageOps.exif_transpose(image)
image = image.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image)[None,]
return image_tensor
def run(self, input_id, images=None, default_value=None, display_name=None, description=None):
import requests
import zipfile
import io
processed_images = [] processed_images = []
try: try:
images_list = json.loads(images) # Assuming images is a JSON array string images_list = json.loads(images) # Assuming images is a JSON array string
print(images_list) print(images_list)
for img_input in images_list: for img_input in images_list:
if img_input.startswith('http') and img_input.endswith('.zip'): if img_input.startswith('http'):
print("Fetching zip file from url: ", img_input) import requests
response = requests.get(img_input)
zip_file = zipfile.ZipFile(io.BytesIO(response.content))
for file_name in zip_file.namelist():
if file_name.lower().endswith(('.png', '.jpg', '.jpeg')):
with zip_file.open(file_name) as file:
image = Image.open(file)
image = self.process_image(image)
processed_images.append(image)
elif img_input.startswith('http'):
from io import BytesIO from io import BytesIO
print("Fetching image from url: ", img_input) print("Fetching image from url: ", img_input)
response = requests.get(img_input) response = requests.get(img_input)
+9 -48
View File
@@ -5,14 +5,6 @@ import torch
import folder_paths import folder_paths
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyUIDeployExternalLora: class ComfyUIDeployExternalLora:
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
@@ -25,69 +17,38 @@ class ComfyUIDeployExternalLora:
}, },
"optional": { "optional": {
"default_lora_name": (folder_paths.get_filename_list("loras"),), "default_lora_name": (folder_paths.get_filename_list("loras"),),
"lora_save_name": ( # if `default_lora_name` is a link to download a file, we will attempt to save it with this name
"STRING",
{"multiline": False, "default": ""},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
"lora_url": (
"STRING",
{"multiline": False, "default": ""},
),
}, },
} }
RETURN_TYPES = (WILDCARD,) RETURN_TYPES = (folder_paths.get_filename_list("loras"),)
RETURN_NAMES = ("path",) RETURN_NAMES = ("path",)
FUNCTION = "run" FUNCTION = "run"
CATEGORY = "deploy" CATEGORY = "deploy"
def run( def run(self, input_id, default_lora_name=None):
self,
input_id,
default_lora_name=None,
lora_save_name=None,
display_name=None,
description=None,
lora_url=None,
):
import requests import requests
import os import os
import uuid import uuid
if lora_url and lora_url.startswith("http"): if default_lora_name.startswith("http"):
if lora_save_name: unique_filename = str(uuid.uuid4()) + ".safetensors"
existing_loras = folder_paths.get_filename_list("loras") print(unique_filename)
# Check if lora_save_name exists in the list
if lora_save_name in existing_loras:
print(f"using lora: {lora_save_name}")
return (lora_save_name,)
else:
lora_save_name = str(uuid.uuid4()) + ".safetensors"
print(lora_save_name)
print(folder_paths.folder_names_and_paths["loras"][0][0]) print(folder_paths.folder_names_and_paths["loras"][0][0])
destination_path = os.path.join( destination_path = os.path.join(
folder_paths.folder_names_and_paths["loras"][0][0], lora_save_name folder_paths.folder_names_and_paths["loras"][0][0], unique_filename
) )
print(destination_path) print(destination_path)
print("Downloading external lora - " + lora_url + " to " + destination_path) print("Downloading external lora - " + input_id + " to " + destination_path)
response = requests.get( response = requests.get(
lora_url, input_id,
headers={"User-Agent": "Mozilla/5.0"}, headers={"User-Agent": "Mozilla/5.0"},
allow_redirects=True, allow_redirects=True,
) )
with open(destination_path, "wb") as out_file: with open(destination_path, "wb") as out_file:
out_file.write(response.content) out_file.write(response.content)
return (lora_save_name,) return (unique_filename,)
else: else:
print(f"using lora: {default_lora_name}") print(f"using lora: {default_lora_name}")
return (default_lora_name,) return (default_lora_name,)
+2 -10
View File
@@ -16,15 +16,7 @@ class ComfyUIDeployExternalNumber:
"optional": { "optional": {
"default_value": ( "default_value": (
"FLOAT", "FLOAT",
{"multiline": True, "display": "number", "default": 0, "min": -2147483647, "max": 2147483647, "step": 0.01}, {"multiline": True, "display": "number", "default": 0, "step": 0.01},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
), ),
} }
} }
@@ -36,7 +28,7 @@ class ComfyUIDeployExternalNumber:
CATEGORY = "number" CATEGORY = "number"
def run(self, input_id, default_value=None, display_name=None, description=None): def run(self, input_id, default_value=None):
try: try:
float_value = float(input_id) float_value = float(input_id)
print("my number", float_value) print("my number", float_value)
+2 -10
View File
@@ -16,15 +16,7 @@ class ComfyUIDeployExternalNumberInt:
"optional": { "optional": {
"default_value": ( "default_value": (
"INT", "INT",
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0}, {"multiline": True, "display": "number", "default": 0},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
), ),
} }
} }
@@ -36,7 +28,7 @@ class ComfyUIDeployExternalNumberInt:
CATEGORY = "number" CATEGORY = "number"
def run(self, input_id, default_value=None, display_name=None, description=None): def run(self, input_id, default_value=None):
if not input_id or (isinstance(input_id, str) and not input_id.strip().isdigit()): if not input_id or (isinstance(input_id, str) and not input_id.strip().isdigit()):
return [default_value] return [default_value]
return [int(input_id)] return [int(input_id)]
+4 -12
View File
@@ -11,23 +11,15 @@ class ComfyUIDeployExternalNumberSlider:
"optional": { "optional": {
"default_value": ( "default_value": (
"FLOAT", "FLOAT",
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0.5, "step": 0.01}, {"multiline": True, "display": "number", "default": 0.5, "step": 0.01},
), ),
"min_value": ( "min_value": (
"FLOAT", "FLOAT",
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0, "step": 0.01}, {"multiline": True, "display": "number", "default": 0, "step": 0.01},
), ),
"max_value": ( "max_value": (
"FLOAT", "FLOAT",
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 1, "step": 0.01}, {"multiline": True, "display": "number", "default": 1, "step": 0.01},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
), ),
} }
} }
@@ -39,7 +31,7 @@ class ComfyUIDeployExternalNumberSlider:
CATEGORY = "number" CATEGORY = "number"
def run(self, input_id, default_value=None, min_value=0, max_value=1, display_name=None, description=None): def run(self, input_id, default_value=None, min_value=0, max_value=1):
try: try:
float_value = float(input_id) float_value = float(input_id)
if min_value <= float_value <= max_value: if min_value <= float_value <= max_value:
+1 -9
View File
@@ -18,14 +18,6 @@ class ComfyUIDeployExternalText:
"STRING", "STRING",
{"multiline": True, "default": ""}, {"multiline": True, "default": ""},
), ),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -36,7 +28,7 @@ class ComfyUIDeployExternalText:
CATEGORY = "text" CATEGORY = "text"
def run(self, input_id, default_value=None, display_name=None, description=None): def run(self, input_id, default_value=None):
return [default_value] return [default_value]
-46
View File
@@ -1,46 +0,0 @@
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyUIDeployExternalTextAny:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_text"},
),
},
"optional": {
"default_value": (
"STRING",
{"multiline": True, "default": ""},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
RETURN_TYPES = (WILDCARD,)
RETURN_NAMES = ("text",)
FUNCTION = "run"
CATEGORY = "text"
def run(self, input_id, default_value=None, display_name=None, description=None):
return [default_value]
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalTextAny": ComfyUIDeployExternalTextAny}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalTextAny": "External Text Any (ComfyUI Deploy)"}
+84 -354
View File
@@ -1,15 +1,10 @@
# credit goes to https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite # credit goes to https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite and is meant to work with
# Intended to work with https://github.com/NicholasKao1029/ComfyUI-VideoHelperSuite/tree/main
import os import os
import itertools import itertools
import numpy as np import numpy as np
import torch import torch
from typing import Union
from torch import Tensor
import cv2 import cv2
import psutil
from collections.abc import Mapping
import folder_paths import folder_paths
from comfy.utils import common_upscale from comfy.utils import common_upscale
@@ -95,25 +90,13 @@ if gifski_path is None:
gifski_path = shutil.which("gifski") gifski_path = shutil.which("gifski")
def is_safe_path(path):
if "VHS_STRICT_PATHS" not in os.environ:
return True
basedir = os.path.abspath(".")
try:
common_path = os.path.commonpath([basedir, path])
except:
# Different drive on windows
return False
return common_path == basedir
def get_sorted_dir_files_from_directory( def get_sorted_dir_files_from_directory(
directory: str, directory: str,
skip_first_images: int = 0, skip_first_images: int = 0,
select_every_nth: int = 1, select_every_nth: int = 1,
extensions: Iterable = None, extensions: Iterable = None,
): ):
directory = strip_path(directory) directory = directory.strip()
dir_files = os.listdir(directory) dir_files = os.listdir(directory)
dir_files = sorted(dir_files) dir_files = sorted(dir_files)
dir_files = [os.path.join(directory, x) for x in dir_files] dir_files = [os.path.join(directory, x) for x in dir_files]
@@ -194,59 +177,18 @@ def requeue_workflow(requeue_required=(-1, True)):
def get_audio(file, start_time=0, duration=0): def get_audio(file, start_time=0, duration=0):
args = [ffmpeg_path, "-i", file] args = [ffmpeg_path, "-v", "error", "-i", file]
if start_time > 0: if start_time > 0:
args += ["-ss", str(start_time)] args += ["-ss", str(start_time)]
if duration > 0: if duration > 0:
args += ["-t", str(duration)] args += ["-t", str(duration)]
try: try:
# TODO: scan for sample rate and maintain
res = subprocess.run( res = subprocess.run(
args + ["-f", "f32le", "-"], capture_output=True, check=True args + ["-f", "wav", "-"], stdout=subprocess.PIPE, check=True
) ).stdout
audio = torch.frombuffer(bytearray(res.stdout), dtype=torch.float32)
match = re.search(", (\\d+) Hz, (\\w+), ", res.stderr.decode("utf-8"))
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise Exception( return False
f"VHS failed to extract audio from {file}:\n" + e.stderr.decode("utf-8") return res
)
if match:
ar = int(match.group(1))
# NOTE: Just throwing an error for other channel types right now
# Will deal with issues if they come
ac = {"mono": 1, "stereo": 2}[match.group(2)]
else:
ar = 44100
ac = 2
audio = audio.reshape((-1, ac)).transpose(0, 1).unsqueeze(0)
return {"waveform": audio, "sample_rate": ar}
class LazyAudioMap(Mapping):
def __init__(self, file, start_time, duration):
self.file = file
self.start_time = start_time
self.duration = duration
self._dict = None
def __getitem__(self, key):
if self._dict is None:
self._dict = get_audio(self.file, self.start_time, self.duration)
return self._dict[key]
def __iter__(self):
if self._dict is None:
self._dict = get_audio(self.file, self.start_time, self.duration)
return iter(self._dict)
def __len__(self):
if self._dict is None:
self._dict = get_audio(self.file, self.start_time, self.duration)
return len(self._dict)
def lazy_get_audio(file, start_time=0, duration=0):
return LazyAudioMap(file, start_time, duration)
def lazy_eval(func): def lazy_eval(func):
@@ -288,19 +230,6 @@ def validate_sequence(path):
return False return False
def strip_path(path):
# This leaves whitespace inside quotes and only a single "
# thus ' ""test"' -> '"test'
# consider path.strip(string.whitespace+"\"")
# or weightier re.fullmatch("[\\s\"]*(.+?)[\\s\"]*", path).group(1)
path = path.strip()
if path.startswith('"'):
path = path[1:]
if path.endswith('"'):
path = path[:-1]
return path
def hash_path(path): def hash_path(path):
if path is None: if path is None:
return "input" return "input"
@@ -357,145 +286,6 @@ def target_size(
return (width, height) return (width, height)
def validate_index(
index: int,
length: int = 0,
is_range: bool = False,
allow_negative=False,
allow_missing=False,
) -> int:
# if part of range, do nothing
if is_range:
return index
# otherwise, validate index
# validate not out of range - only when latent_count is passed in
if length > 0 and index > length - 1 and not allow_missing:
raise IndexError(f"Index '{index}' out of range for {length} item(s).")
# if negative, validate not out of range
if index < 0:
if not allow_negative:
raise IndexError(f"Negative indeces not allowed, but was '{index}'.")
conv_index = length + index
if conv_index < 0 and not allow_missing:
raise IndexError(
f"Index '{index}', converted to '{conv_index}' out of range for {length} item(s)."
)
index = conv_index
return index
def convert_to_index_int(
raw_index: str,
length: int = 0,
is_range: bool = False,
allow_negative=False,
allow_missing=False,
) -> int:
try:
return validate_index(
int(raw_index),
length=length,
is_range=is_range,
allow_negative=allow_negative,
allow_missing=allow_missing,
)
except ValueError as e:
raise ValueError(f"Index '{raw_index}' must be an integer.", e)
def convert_str_to_indexes(
indexes_str: str, length: int = 0, allow_missing=False
) -> list[int]:
if not indexes_str:
return []
int_indexes = list(range(0, length))
allow_negative = length > 0
chosen_indexes = []
# parse string - allow positive ints, negative ints, and ranges separated by ':'
groups = indexes_str.split(",")
groups = [g.strip() for g in groups]
for g in groups:
# parse range of indeces (e.g. 2:16)
if ":" in g:
index_range = g.split(":", 2)
index_range = [r.strip() for r in index_range]
start_index = index_range[0]
if len(start_index) > 0:
start_index = convert_to_index_int(
start_index,
length=length,
is_range=True,
allow_negative=allow_negative,
allow_missing=allow_missing,
)
else:
start_index = 0
end_index = index_range[1]
if len(end_index) > 0:
end_index = convert_to_index_int(
end_index,
length=length,
is_range=True,
allow_negative=allow_negative,
allow_missing=allow_missing,
)
else:
end_index = length
# support step as well, to allow things like reversing, every-other, etc.
step = 1
if len(index_range) > 2:
step = index_range[2]
if len(step) > 0:
step = convert_to_index_int(
step,
length=length,
is_range=True,
allow_negative=True,
allow_missing=True,
)
else:
step = 1
# if latents were passed in, base indeces on known latent count
if len(int_indexes) > 0:
chosen_indexes.extend(int_indexes[start_index:end_index][::step])
# otherwise, assume indeces are valid
else:
chosen_indexes.extend(list(range(start_index, end_index, step)))
# parse individual indeces
else:
chosen_indexes.append(
convert_to_index_int(
g,
length=length,
allow_negative=allow_negative,
allow_missing=allow_missing,
)
)
return chosen_indexes
def select_indexes(input_obj: Union[Tensor, list], idxs: list):
if type(input_obj) == Tensor:
return input_obj[idxs]
else:
return [input_obj[i] for i in idxs]
def select_indexes_from_str(
input_obj: Union[Tensor, list], indexes: str, err_if_missing=True, err_if_empty=True
):
real_idxs = convert_str_to_indexes(
indexes, len(input_obj), allow_missing=not err_if_missing
)
if err_if_empty and len(real_idxs) == 0:
raise Exception(f"Nothing was selected based on indexes found in '{indexes}'.")
return select_indexes(input_obj, real_idxs)
###
def cv_frame_generator( def cv_frame_generator(
video, video,
force_rate, force_rate,
@@ -505,10 +295,9 @@ def cv_frame_generator(
meta_batch=None, meta_batch=None,
unique_id=None, unique_id=None,
): ):
video_cap = cv2.VideoCapture(strip_path(video)) video_cap = cv2.VideoCapture(video)
if not video_cap.isOpened(): if not video_cap.isOpened():
raise ValueError(f"{video} could not be loaded with cv.") raise ValueError(f"{video} could not be loaded with cv.")
pbar = None
# extract video metadata # extract video metadata
fps = video_cap.get(cv2.CAP_PROP_FPS) fps = video_cap.get(cv2.CAP_PROP_FPS)
@@ -530,8 +319,6 @@ def cv_frame_generator(
target_frame_time = 1 / force_rate target_frame_time = 1 / force_rate
yield (width, height, fps, duration, total_frames, target_frame_time) yield (width, height, fps, duration, total_frames, target_frame_time)
if meta_batch is not None:
yield min(frame_load_cap, total_frames)
time_offset = target_frame_time - base_frame_time time_offset = target_frame_time - base_frame_time
while video_cap.isOpened(): while video_cap.isOpened():
@@ -562,8 +349,7 @@ def cv_frame_generator(
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# convert frame to comfyui's expected format # convert frame to comfyui's expected format
# TODO: frame contains no exif information. Check if opencv2 has already applied # TODO: frame contains no exif information. Check if opencv2 has already applied
frame = np.array(frame, dtype=np.float32) frame = np.array(frame, dtype=np.float32) / 255.0
torch.from_numpy(frame).div_(255)
if prev_frame is not None: if prev_frame is not None:
inp = yield prev_frame inp = yield prev_frame
if inp is not None: if inp is not None:
@@ -571,8 +357,6 @@ def cv_frame_generator(
return return
prev_frame = frame prev_frame = frame
frames_added += 1 frames_added += 1
if pbar is not None:
pbar.update_absolute(frames_added, frame_load_cap)
# if cap exists and we've reached it, stop processing frames # if cap exists and we've reached it, stop processing frames
if frame_load_cap > 0 and frames_added >= frame_load_cap: if frame_load_cap > 0 and frames_added >= frame_load_cap:
break break
@@ -583,17 +367,6 @@ def cv_frame_generator(
yield prev_frame yield prev_frame
def batched(it, n):
while batch := tuple(itertools.islice(it, n)):
yield batch
def batched_vae_encode(images, vae, frames_per_batch):
for batch in batched(images, frames_per_batch):
image_batch = torch.from_numpy(np.array(batch))
yield from vae.encode(image_batch).numpy()
def load_video_cv( def load_video_cv(
video: str, video: str,
force_rate: int, force_rate: int,
@@ -605,8 +378,6 @@ def load_video_cv(
select_every_nth: int, select_every_nth: int,
meta_batch=None, meta_batch=None,
unique_id=None, unique_id=None,
memory_limit_mb=None,
vae=None,
): ):
if meta_batch is None or unique_id not in meta_batch.inputs: if meta_batch is None or unique_id not in meta_batch.inputs:
gen = cv_frame_generator( gen = cv_frame_generator(
@@ -630,89 +401,30 @@ def load_video_cv(
total_frames, total_frames,
target_frame_time, target_frame_time,
) )
meta_batch.total_frames = min(meta_batch.total_frames, next(gen))
else: else:
(gen, width, height, fps, duration, total_frames, target_frame_time) = ( (gen, width, height, fps, duration, total_frames, target_frame_time) = (
meta_batch.inputs[unique_id] meta_batch.inputs[unique_id]
) )
memory_limit = None if meta_batch is not None:
if memory_limit_mb is not None: gen = itertools.islice(gen, meta_batch.frames_per_batch)
memory_limit *= 2**20
else:
# TODO: verify if garbage collection should be performed here.
# leaves ~128 MB unreserved for safety
try:
memory_limit = (
psutil.virtual_memory().available + psutil.swap_memory().free
) - 2**27
except:
print(
"Failed to calculate available memory. Memory load limit has been disabled"
)
if memory_limit is not None:
if vae is not None:
# space required to load as f32, exist as latent with wiggle room, decode to f32
max_loadable_frames = int(
memory_limit // (width * height * 3 * (4 + 4 + 1 / 10))
)
else:
# TODO: use better estimate for when vae is not None
# Consider completely ignoring for load_latent case?
max_loadable_frames = int(memory_limit // (width * height * 3 * (0.1)))
if meta_batch is not None:
if meta_batch.frames_per_batch > max_loadable_frames:
raise RuntimeError(
f"Meta Batch set to {meta_batch.frames_per_batch} frames but only {max_loadable_frames} can fit in memory"
)
gen = itertools.islice(gen, meta_batch.frames_per_batch)
else:
original_gen = gen
gen = itertools.islice(gen, max_loadable_frames)
downscale_ratio = getattr(vae, "downscale_ratio", 8)
frames_per_batch = (1920 * 1080 * 16) // (width * height) or 1
if force_size != "Disabled" or vae is not None:
new_size = target_size(
width, height, force_size, custom_width, custom_height, downscale_ratio
)
if new_size[0] != width or new_size[1] != height:
def rescale(frame): # Some minor wizardry to eliminate a copy and reduce max memory by a factor of ~2
s = torch.from_numpy( images = torch.from_numpy(
np.fromiter(frame, np.dtype((np.float32, (height, width, 3)))) np.fromiter(gen, np.dtype((np.float32, (height, width, 3))))
) )
s = s.movedim(-1, 1)
s = common_upscale(s, new_size[0], new_size[1], "lanczos", "center")
return s.movedim(1, -1).numpy()
gen = itertools.chain.from_iterable(
map(rescale, batched(gen, frames_per_batch))
)
else:
new_size = width, height
if vae is not None:
gen = batched_vae_encode(gen, vae, frames_per_batch)
vw, vh = new_size[0] // downscale_ratio, new_size[1] // downscale_ratio
images = torch.from_numpy(np.fromiter(gen, np.dtype((np.float32, (4, vh, vw)))))
else:
# Some minor wizardry to eliminate a copy and reduce max memory by a factor of ~2
images = torch.from_numpy(
np.fromiter(gen, np.dtype((np.float32, (new_size[1], new_size[0], 3))))
)
if meta_batch is None and memory_limit is not None:
try:
next(original_gen)
raise RuntimeError(
f"Memory limit hit after loading {len(images)} frames. Stopping execution."
)
except StopIteration:
pass
if len(images) == 0: if len(images) == 0:
raise RuntimeError("No frames generated") raise RuntimeError("No frames generated")
if force_size != "Disabled":
new_size = target_size(width, height, force_size, custom_width, custom_height)
if new_size[0] != width or new_size[1] != height:
s = images.movedim(-1, 1)
s = common_upscale(s, new_size[0], new_size[1], "lanczos", "center")
images = s.movedim(1, -1)
# Setup lambda for lazy audio capture # Setup lambda for lazy audio capture
audio = lazy_get_audio( audio = lambda: get_audio(
video, video,
skip_first_frames * target_frame_time, skip_first_frames * target_frame_time,
frame_load_cap * target_frame_time * select_every_nth, frame_load_cap * target_frame_time * select_every_nth,
@@ -728,16 +440,13 @@ def load_video_cv(
"loaded_fps": 1 / target_frame_time, "loaded_fps": 1 / target_frame_time,
"loaded_frame_count": len(images), "loaded_frame_count": len(images),
"loaded_duration": len(images) * target_frame_time, "loaded_duration": len(images) * target_frame_time,
"loaded_width": new_size[0], "loaded_width": images.shape[2],
"loaded_height": new_size[1], "loaded_height": images.shape[1],
} }
if vae is None:
return (images, len(images), audio, video_info, None) return (images, len(images), lazy_eval(audio), video_info)
else:
return (None, len(images), audio, video_info, {"samples": images})
# modeled after Video upload node
class ComfyUIDeployExternalVideo: class ComfyUIDeployExternalVideo:
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
@@ -748,46 +457,68 @@ class ComfyUIDeployExternalVideo:
file_parts = f.split(".") file_parts = f.split(".")
if len(file_parts) > 1 and (file_parts[-1] in video_extensions): if len(file_parts) > 1 and (file_parts[-1] in video_extensions):
files.append(f) files.append(f)
return {"required": { return {
"input_id": ( "required": {
"STRING", "input_id": (
{"multiline": False, "default": "input_video"}, "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"],), "force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1}),
"custom_width": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}), "force_size": (
"custom_height": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}), [
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}), "Disabled",
"skip_first_frames": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}), "Custom Height",
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}), "Custom Width",
}, "Custom",
"optional": { "256x?",
"meta_batch": ("VHS_BatchManager",), "?x256",
"vae": ("VAE",), "256x256",
"default_video": (sorted(files),), "512x?",
"display_name": ( "?x512",
"STRING", "512x512",
{"multiline": False, "default": ""}, ],
), ),
"description": ( "custom_width": (
"STRING", "INT",
{"multiline": True, "default": ""}, {"default": 512, "min": 0, "max": DIMMAX, "step": 8},
), ),
}, "custom_height": (
"hidden": { "INT",
"unique_id": "UNIQUE_ID" {"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",),
"default_value": (sorted(files),),
},
"hidden": {"unique_id": "UNIQUE_ID"},
}
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢" CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
RETURN_TYPES = ("IMAGE", "INT", "AUDIO", "VHS_VIDEOINFO", "LATENT") RETURN_TYPES = (
"IMAGE",
"INT",
"VHS_AUDIO",
"VHS_VIDEOINFO",
)
RETURN_NAMES = ( RETURN_NAMES = (
"IMAGE", "IMAGE",
"frame_count", "frame_count",
"audio", "audio",
"video_info", "video_info",
"LATENT",
) )
FUNCTION = "load_video" FUNCTION = "load_video"
@@ -804,6 +535,8 @@ class ComfyUIDeployExternalVideo:
meta_batch = kwargs.get("meta_batch") meta_batch = kwargs.get("meta_batch")
unique_id = kwargs.get("unique_id") unique_id = kwargs.get("unique_id")
video = kwargs.get("default_value")
video_path = folder_paths.get_annotated_filepath(video.strip('"'))
input_dir = folder_paths.get_input_directory() input_dir = folder_paths.get_input_directory()
if input_id.startswith("http"): if input_id.startswith("http"):
@@ -833,11 +566,8 @@ class ComfyUIDeployExternalVideo:
leave=True, leave=True,
): ):
out_file.write(chunk) out_file.write(chunk)
else:
video = kwargs.get("default_video", None) print("video path: ", video_path)
if video is None:
raise "No default video given and no external video provided"
video_path = folder_paths.get_annotated_filepath(video.strip('"'))
return load_video_cv( return load_video_cv(
video=video_path, video=video_path,
-60
View File
@@ -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 = "model"
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)"}
+381 -990
View File
File diff suppressed because it is too large Load Diff
+8 -28
View File
@@ -6,12 +6,10 @@ from PIL import Image, ImageOps
from io import BytesIO from io import BytesIO
from pydantic import BaseModel as PydanticBaseModel from pydantic import BaseModel as PydanticBaseModel
class BaseModel(PydanticBaseModel): class BaseModel(PydanticBaseModel):
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
class Status(Enum): class Status(Enum):
NOT_STARTED = "not-started" NOT_STARTED = "not-started"
RUNNING = "running" RUNNING = "running"
@@ -19,7 +17,6 @@ class Status(Enum):
FAILED = "failed" FAILED = "failed"
UPLOADING = "uploading" UPLOADING = "uploading"
class StreamingPrompt(BaseModel): class StreamingPrompt(BaseModel):
workflow_api: Any workflow_api: Any
auth_token: str auth_token: str
@@ -27,52 +24,42 @@ class StreamingPrompt(BaseModel):
running_prompt_ids: set[str] = set() running_prompt_ids: set[str] = set()
status_endpoint: Optional[str] status_endpoint: Optional[str]
file_upload_endpoint: Optional[str] file_upload_endpoint: Optional[str]
workflow: Any
gpu_event_id: Optional[str] = None
class SimplePrompt(BaseModel): class SimplePrompt(BaseModel):
status_endpoint: Optional[str] status_endpoint: Optional[str]
file_upload_endpoint: Optional[str] file_upload_endpoint: Optional[str]
token: Optional[str]
workflow_api: dict workflow_api: dict
status: Status = Status.NOT_STARTED status: Status = Status.NOT_STARTED
progress: set = set() progress: set = set()
last_updated_node: Optional[str] = None last_updated_node: Optional[str] = None,
uploading_nodes: set = set() uploading_nodes: set = set()
done: bool = False done: bool = False
is_realtime: bool = False is_realtime: bool = False,
start_time: Optional[float] = None start_time: Optional[float] = None,
gpu_event_id: Optional[str] = None
sockets = dict() sockets = dict()
prompt_metadata: dict[str, SimplePrompt] = {} prompt_metadata: dict[str, SimplePrompt] = {}
streaming_prompt_metadata: dict[str, StreamingPrompt] = {} streaming_prompt_metadata: dict[str, StreamingPrompt] = {}
class BinaryEventTypes: class BinaryEventTypes:
PREVIEW_IMAGE = 1 PREVIEW_IMAGE = 1
UNENCODED_PREVIEW_IMAGE = 2 UNENCODED_PREVIEW_IMAGE = 2
max_output_id_length = 24 max_output_id_length = 24
async def send_image(image_data, sid=None, output_id:str = None):
async def send_image(image_data, sid=None, output_id: str = None):
max_length = max_output_id_length max_length = max_output_id_length
output_id = output_id[:max_length] output_id = output_id[:max_length]
padded_output_id = output_id.ljust(max_length, "\x00") padded_output_id = output_id.ljust(max_length, '\x00')
encoded_output_id = padded_output_id.encode("ascii", "replace") encoded_output_id = padded_output_id.encode('ascii', 'replace')
image_type = image_data[0] image_type = image_data[0]
image = image_data[1] image = image_data[1]
max_size = image_data[2] max_size = image_data[2]
quality = image_data[3] quality = image_data[3]
if max_size is not None: if max_size is not None:
if hasattr(Image, "Resampling"): if hasattr(Image, 'Resampling'):
resampling = Image.Resampling.BILINEAR resampling = Image.Resampling.BILINEAR
else: else:
resampling = Image.ANTIALIAS resampling = Image.ANTIALIAS
@@ -101,18 +88,12 @@ async def send_image(image_data, sid=None, output_id: str = None):
preview_bytes = bytesIO.getvalue() preview_bytes = bytesIO.getvalue()
await send_bytes(BinaryEventTypes.PREVIEW_IMAGE, preview_bytes, sid=sid) await send_bytes(BinaryEventTypes.PREVIEW_IMAGE, preview_bytes, sid=sid)
async def send_socket_catch_exception(function, message): async def send_socket_catch_exception(function, message):
try: try:
await function(message) await function(message)
except ( except (aiohttp.ClientError, aiohttp.ClientPayloadError, ConnectionResetError) as err:
aiohttp.ClientError,
aiohttp.ClientPayloadError,
ConnectionResetError,
) as err:
print("send error:", err) print("send error:", err)
def encode_bytes(event, data): def encode_bytes(event, data):
if not isinstance(event, int): if not isinstance(event, int):
raise RuntimeError(f"Binary event types must be integers, got {event}") raise RuntimeError(f"Binary event types must be integers, got {event}")
@@ -122,7 +103,6 @@ def encode_bytes(event, data):
message.extend(data) message.extend(data)
return message return message
async def send_bytes(event, data, sid=None): async def send_bytes(event, data, sid=None):
message = encode_bytes(event, data) message = encode_bytes(event, data)
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-deploy" name = "comfyui-deploy"
description = "Open source comfyui deployment platform, a vercel for generative workflow infra." description = "Open source comfyui deployment platform, a vercel for generative workflow infra."
version = "1.1.0" version = "1.0.0"
license = "LICENSE" license = "LICENSE"
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg"] dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg"]
+1 -2
View File
@@ -2,5 +2,4 @@ aiofiles
pydantic pydantic
opencv-python opencv-python
imageio-ffmpeg imageio-ffmpeg
brotli logfire
# logfire
+67 -486
View File
@@ -2,7 +2,6 @@ import { app } from "./app.js";
import { api } from "./api.js"; import { api } from "./api.js";
import { ComfyWidgets, LGraphNode } from "./widgets.js"; import { ComfyWidgets, LGraphNode } from "./widgets.js";
import { generateDependencyGraph } from "https://esm.sh/[email protected]"; import { generateDependencyGraph } from "https://esm.sh/[email protected]";
import { ComfyDeploy } from "https://esm.sh/[email protected]";
const loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`; const loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
@@ -20,8 +19,11 @@ function dispatchAPIEventData(data) {
// Custom parse error // Custom parse error
if (msg.error) { if (msg.error) {
let message = msg.error.message; let message = msg.error.message;
if (msg.error.details) message += ": " + msg.error.details; if (msg.error.details)
for (const [nodeID, nodeError] of Object.entries(msg.node_errors)) { message += ": " + msg.error.details;
for (const [nodeID, nodeError] of Object.entries(
msg.node_errors,
)) {
message += "\n" + nodeError.class_type + ":"; message += "\n" + nodeError.class_type + ":";
for (const errorReason of nodeError.errors) { for (const errorReason of nodeError.errors) {
message += message +=
@@ -83,52 +85,11 @@ function dispatchAPIEventData(data) {
} }
} }
const context = {
selectedWorkflowInfo: null,
};
// let selectedWorkflowInfo = {
// workflow_id: "05da8f2b-63af-4c0c-86dd-08d01ec512b7",
// machine_id: "45ac5f85-b7b6-436f-8d97-2383b25485f3",
// native_run_api_endpoint: "http://localhost:3011/api/run",
// };
async function getSelectedWorkflowInfo() {
const workflow_info_promise = new Promise((resolve) => {
try {
const handleMessage = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === "workflow_info") {
resolve(message.data);
window.removeEventListener("message", handleMessage);
}
} catch (error) {
console.error(error);
resolve(undefined);
}
};
window.addEventListener("message", handleMessage);
sendEventToCD("workflow_info");
} catch (error) {
console.error(error);
resolve(undefined);
}
});
return workflow_info_promise;
}
function setSelectedWorkflowInfo(info) {
context.selectedWorkflowInfo = info;
}
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ /** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
/** @type {ComfyExtension} */ /** @type {ComfyExtension} */
const ext = { const ext = {
name: "BennyKok.ComfyUIDeploy", name: "BennyKok.ComfyUIDeploy",
native_mode: false,
init(app) { init(app) {
addButton(); addButton();
@@ -138,17 +99,16 @@ const ext = {
const org_display = queryParams.get("org_display"); const org_display = queryParams.get("org_display");
const origin = queryParams.get("origin"); const origin = queryParams.get("origin");
const workspace_mode = queryParams.get("workspace_mode"); const workspace_mode = queryParams.get("workspace_mode");
this.native_mode = queryParams.get("native_mode") === "true";
if (workspace_mode) { if (workspace_mode) {
document.querySelector(".comfy-menu").style.display = "none"; document.querySelector(".comfy-menu").style.display = "none";
sendEventToCD("cd_plugin_onInit"); sendEventToCD("cd_plugin_onInit");
// app.queuePrompt = ((originalFunction) => async () => { app.queuePrompt = ((originalFunction) => async () => {
// // const prompt = await app.graphToPrompt(); // const prompt = await app.graphToPrompt();
// sendEventToCD("cd_plugin_onQueuePromptTrigger"); sendEventToCD("cd_plugin_onQueuePromptTrigger");
// })(app.queuePrompt); })(app.queuePrompt);
// // Intercept the onkeydown event // // Intercept the onkeydown event
// window.addEventListener( // window.addEventListener(
@@ -234,13 +194,11 @@ const ext = {
registerCustomNodes() { registerCustomNodes() {
/** @type {LGraphNode}*/ /** @type {LGraphNode}*/
class ComfyDeploy extends LGraphNode { class ComfyDeploy {
color = LGraphCanvas.node_colors.yellow.color;
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
constructor() { constructor() {
super();
this.color = LGraphCanvas.node_colors.yellow.color;
this.bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
this.groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
if (!this.properties) { if (!this.properties) {
this.properties = {}; this.properties = {};
this.properties.workflow_name = ""; this.properties.workflow_name = "";
@@ -248,75 +206,50 @@ const ext = {
this.properties.version = ""; this.properties.version = "";
} }
this.addWidget( ComfyWidgets.STRING(
"text", this,
"workflow_name", "workflow_name",
this.properties.workflow_name, ["", { default: this.properties.workflow_name, multiline: false }],
(v) => { app,
this.properties.workflow_name = v;
},
{ multiline: false },
); );
this.addWidget( ComfyWidgets.STRING(
"text", this,
"workflow_id", "workflow_id",
this.properties.workflow_id, ["", { default: this.properties.workflow_id, multiline: false }],
(v) => { app,
this.properties.workflow_id = v;
},
{ multiline: false },
); );
this.addWidget( ComfyWidgets.STRING(
"text", this,
"version", "version",
this.properties.version, ["", { default: this.properties.version, multiline: false }],
(v) => { app,
this.properties.version = v;
},
{ multiline: false },
); );
// this.widgets.forEach((w) => {
// // w.computeSize = () => [200,10]
// w.computedHeight = 2;
// })
this.widgets_start_y = 10; this.widgets_start_y = 10;
this.setSize(this.computeSize());
// const config = { };
// console.log(this);
this.serialize_widgets = true; this.serialize_widgets = true;
this.isVirtualNode = true; this.isVirtualNode = true;
} }
onExecute() {
// This method is called when the node is executed
// You can add any necessary logic here
}
onSerialize(o) {
// This method is called when the node is being serialized
// Ensure all necessary data is saved
if (!o.properties) {
o.properties = {};
}
o.properties.workflow_name = this.properties.workflow_name;
o.properties.workflow_id = this.properties.workflow_id;
o.properties.version = this.properties.version;
}
onConfigure(o) {
// This method is called when the node is being configured (e.g., when loading a saved graph)
// Ensure all necessary data is restored
if (o.properties) {
this.properties = { ...this.properties, ...o.properties };
this.widgets[0].value = this.properties.workflow_name || "";
this.widgets[1].value = this.properties.workflow_id || "";
this.widgets[2].value = this.properties.version || "1";
}
}
} }
// Register the node type // Load default visibility
LiteGraph.registerNodeType( LiteGraph.registerNodeType(
"ComfyDeploy", "ComfyDeploy",
Object.assign(ComfyDeploy, { Object.assign(ComfyDeploy, {
title: "Comfy Deploy",
title_mode: LiteGraph.NORMAL_TITLE, title_mode: LiteGraph.NORMAL_TITLE,
title: "Comfy Deploy",
collapsable: true, collapsable: true,
}), }),
); );
@@ -328,116 +261,26 @@ const ext = {
// const graphCanvas = document.getElementById("graph-canvas"); // const graphCanvas = document.getElementById("graph-canvas");
window.addEventListener("message", async (event) => { window.addEventListener("message", async (event) => {
// console.log("message", event);
try { try {
const message = JSON.parse(event.data); const message = JSON.parse(event.data);
if (message.type === "graph_load") { if (message.type === "graph_load") {
const comfyUIWorkflow = message.data; const comfyUIWorkflow = message.data;
// console.log("recieved: ", comfyUIWorkflow); console.log("recieved: ", comfyUIWorkflow);
// Assuming there's a method to load the workflow data into the ComfyUI // Assuming there's a method to load the workflow data into the ComfyUI
// This part of the code would depend on how the ComfyUI expects to receive and process the workflow data // This part of the code would depend on how the ComfyUI expects to receive and process the workflow data
// For demonstration, let's assume there's a loadWorkflow method in the ComfyUI API // For demonstration, let's assume there's a loadWorkflow method in the ComfyUI API
if (comfyUIWorkflow && app && app.loadGraphData) { if (comfyUIWorkflow && app && app.loadGraphData) {
try {
await window["app"].ui.settings.setSettingValueAsync(
"Comfy.Validation.Workflows",
false,
);
} catch (error) {
console.warning(
"Error setting validation to false, is fine to ignore this",
error,
);
}
console.log("loadGraphData");
app.loadGraphData(comfyUIWorkflow); app.loadGraphData(comfyUIWorkflow);
} }
} else if (message.type === "deploy") { } else if (message.type === "deploy") {
// deployWorkflow(); // deployWorkflow();
const prompt = await app.graphToPrompt(); const prompt = await app.graphToPrompt();
// api.handlePromptGenerated(prompt);
sendEventToCD("cd_plugin_onDeployChanges", prompt); sendEventToCD("cd_plugin_onDeployChanges", prompt);
} else if (message.type === "queue_prompt") { } else if (message.type === "queue_prompt") {
const prompt = await app.graphToPrompt(); const prompt = await app.graphToPrompt();
if (typeof api.handlePromptGenerated === "function") {
api.handlePromptGenerated(prompt);
} else {
console.warn("api.handlePromptGenerated is not a function");
}
sendEventToCD("cd_plugin_onQueuePrompt", prompt); sendEventToCD("cd_plugin_onQueuePrompt", prompt);
} else if (message.type === "get_prompt") {
const prompt = await app.graphToPrompt();
sendEventToCD("cd_plugin_onGetPrompt", prompt);
} else if (message.type === "event") { } else if (message.type === "event") {
dispatchAPIEventData(message.data); dispatchAPIEventData(message.data);
} else if (message.type === "add_node") {
console.log("add node", message.data);
app.graph.beforeChange();
var node = LiteGraph.createNode(message.data.type);
node.configure({
widgets_values: message.data.widgets_values,
});
console.log("node", node);
const graphMouse = app.canvas.graph_mouse;
node.pos = [graphMouse[0], graphMouse[1]];
app.graph.add(node);
app.graph.afterChange();
} else if (message.type === "zoom_to_node") {
const nodeId = message.data.nodeId;
const position = message.data.position;
const node = app.graph.getNodeById(nodeId);
if (!node) return;
const canvas = app.canvas;
const targetScale = 1;
const targetOffsetX =
canvas.canvas.width / 4 - position[0] - node.size[0] / 2;
const targetOffsetY =
canvas.canvas.height / 4 - position[1] - node.size[1] / 2;
const startScale = canvas.ds.scale;
const startOffsetX = canvas.ds.offset[0];
const startOffsetY = canvas.ds.offset[1];
const duration = 400; // Animation duration in milliseconds
const startTime = Date.now();
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function lerp(start, end, t) {
return start * (1 - t) + end * t;
}
function animate() {
const currentTime = Date.now();
const elapsedTime = currentTime - startTime;
const t = Math.min(elapsedTime / duration, 1);
const easedT = easeOutCubic(t);
const currentScale = lerp(startScale, targetScale, easedT);
const currentOffsetX = lerp(startOffsetX, targetOffsetX, easedT);
const currentOffsetY = lerp(startOffsetY, targetOffsetY, easedT);
canvas.setZoom(currentScale);
canvas.ds.offset = [currentOffsetX, currentOffsetY];
canvas.draw(true, true);
if (t < 1) {
requestAnimationFrame(animate);
}
}
animate();
} else if (message.type === "workflow_info") {
setSelectedWorkflowInfo(message.data);
} }
// else if (message.type === "refresh") { // else if (message.type === "refresh") {
// sendEventToCD("cd_plugin_onRefresh"); // sendEventToCD("cd_plugin_onRefresh");
@@ -445,6 +288,10 @@ const ext = {
} catch (error) { } catch (error) {
// console.error("Error processing message:", error); // console.error("Error processing message:", error);
} }
// if (!event.data.flow || Object.entries(event.data.flow).length <= 0)
// return;
// updateBlendshapesPrompts(event.data.flow);
}); });
api.addEventListener("executed", (evt) => { api.addEventListener("executed", (evt) => {
@@ -457,25 +304,6 @@ const ext = {
// } // }
}); });
if (this.native_mode) {
// console.log("native mode", window, window.app);
try {
await app.ui.settings.setSettingValueAsync("Comfy.UseNewMenu", "Top");
await app.ui.settings.setSettingValueAsync(
"Comfy.Sidebar.Size",
"small"
);
await app.ui.settings.setSettingValueAsync(
"Comfy.Sidebar.Location",
"right"
);
localStorage.setItem("Comfy.MenuPosition.Docked", "true");
console.log("native mode manmanman");
} catch (error) {
console.error("Error setting validation to false", error);
}
}
app.graph.onAfterChange = ((originalFunction) => app.graph.onAfterChange = ((originalFunction) =>
async function () { async function () {
const prompt = await app.graphToPrompt(); const prompt = await app.graphToPrompt();
@@ -587,7 +415,6 @@ function createDynamicUIHtml(data) {
return html; return html;
} }
// Modify the existing deployWorkflow function
async function deployWorkflow() { async function deployWorkflow() {
const deploy = document.getElementById("deploy-button"); const deploy = document.getElementById("deploy-button");
@@ -734,30 +561,30 @@ async function deployWorkflow() {
console.log(hash); console.log(hash);
return hash.file_hash; return hash.file_hash;
}, },
// handleFileUpload: async (file, hash, prevhash) => { handleFileUpload: async (file, hash, prevhash) => {
// console.log("Uploading ", file); console.log("Uploading ", file);
// loadingDialog.showLoading("Uploading file", file); loadingDialog.showLoading("Uploading file", file);
// try { try {
// const { download_url } = await fetch(`/comfyui-deploy/upload-file`, { const { download_url } = await fetch(`/comfyui-deploy/upload-file`, {
// method: "POST", method: "POST",
// body: JSON.stringify({ body: JSON.stringify({
// file_path: file, file_path: file,
// token: apiKey, token: apiKey,
// url: endpoint + "/api/upload-url", url: endpoint + "/api/upload-url",
// }), }),
// }) })
// .then((x) => x.json()) .then((x) => x.json())
// .catch(() => { .catch(() => {
// loadingDialog.close(); loadingDialog.close();
// confirmDialog.confirm("Error", "Unable to upload file " + file); confirmDialog.confirm("Error", "Unable to upload file " + file);
// }); });
// loadingDialog.showLoading("Uploaded file", file); loadingDialog.showLoading("Uploaded file", file);
// console.log(download_url); console.log(download_url);
// return download_url; return download_url;
// } catch (error) { } catch (error) {
// return undefined; return undefined;
// } }
// }, },
existingDependencies: existing_workflow.dependencies, existingDependencies: existing_workflow.dependencies,
}); });
@@ -782,15 +609,6 @@ async function deployWorkflow() {
"Check dependencies", "Check dependencies",
// JSON.stringify(deps, null, 2), // JSON.stringify(deps, null, 2),
` `
<div>
You will need to create a cloud machine with the following configuration on ComfyDeploy
<ol style="text-align: left; margin-top: 10px;">
<li>Review the dependencies listed in the graph below</li>
<li>Create a new cloud machine with the required configuration</li>
<li>Install missing models and check missing files</li>
<li>Deploy your workflow to the newly created machine</li>
</ol>
</div>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div> <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
<iframe <iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;" style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
@@ -860,14 +678,6 @@ async function deployWorkflow() {
`<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`, `<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`,
); );
// // Refresh the workflows list in the sidebar
// const sidebarEl = document.querySelector(
// '.comfy-sidebar-tab[data-id="search"]',
// );
// if (sidebarEl) {
// refreshWorkflowsList(sidebarEl);
// }
setTimeout(() => { setTimeout(() => {
title.textContent = "Deploy"; title.textContent = "Deploy";
title.style.color = "white"; title.style.color = "white";
@@ -885,85 +695,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() { function addButton() {
const menu = document.querySelector(".comfy-menu"); const menu = document.querySelector(".comfy-menu");
@@ -1456,153 +1187,3 @@ export class ConfigDialog extends ComfyDialog {
} }
export const configDialog = new ConfigDialog(); export const configDialog = new ConfigDialog();
const currentOrigin = window.location.origin;
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">
<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>
<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 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");
refreshWorkflowsList(el);
},
});
function getTimeAgo(date) {
const seconds = Math.floor((new Date() - date) / 1000);
let interval = seconds / 31536000;
if (interval > 1) return Math.floor(interval) + " years ago";
interval = seconds / 2592000;
if (interval > 1) return Math.floor(interval) + " months ago";
interval = seconds / 86400;
if (interval > 1) return Math.floor(interval) + " days ago";
interval = seconds / 3600;
if (interval > 1) return Math.floor(interval) + " hours ago";
interval = seconds / 60;
if (interval > 1) return Math.floor(interval) + " minutes ago";
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",
false,
);
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) => {
console.log("Fetch API called with args:", route, options, ext.native_mode);
if (route.startsWith("/prompt") && ext.native_mode) {
const info = await getSelectedWorkflowInfo();
console.log("info", info);
if (info) {
const body = JSON.parse(options.body);
const data = {
client_id: body.client_id,
workflow_api_json: body.prompt,
workflow: body?.extra_data?.extra_pnginfo?.workflow,
is_native_run: true,
machine_id: info.machine_id,
workflow_id: info.workflow_id,
native_run_api_endpoint: info.native_run_api_endpoint,
gpu_event_id: info.gpu_event_id,
};
return await fetch("/comfyui-deploy/run", {
method: "POST",
headers: {
Authorization: `Bearer ${info.cd_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
}
}
return await orginal_fetch_api.call(api, route, options);
};
+1 -1
View File
@@ -74,7 +74,7 @@
"mitata": "^0.1.6", "mitata": "^0.1.6",
"ms": "^2.1.3", "ms": "^2.1.3",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"next": "14.2", "next": "14.1",
"next-plausible": "^3.12.0", "next-plausible": "^3.12.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"next-usequerystate": "^1.13.2", "next-usequerystate": "^1.13.2",
-1
View File
@@ -6,5 +6,4 @@ export const customInputNodes: Record<string, string> = {
ComfyUIDeployExternalNumberInt: "integer", ComfyUIDeployExternalNumberInt: "integer",
ComfyUIDeployExternalLora: "string - (public lora download url)", ComfyUIDeployExternalLora: "string - (public lora download url)",
ComfyUIDeployExternalCheckpoint: "string - (public checkpoints download url)", ComfyUIDeployExternalCheckpoint: "string - (public checkpoints download url)",
ComfyUIDeployExternalFaceModel: "string - (public face model download url)",
}; };
+1 -3
View File
@@ -51,9 +51,7 @@ const createRunRoute = createRoute({
export const registerCreateRunRoute = (app: App) => { export const registerCreateRunRoute = (app: App) => {
app.openapi(createRunRoute, async (c) => { app.openapi(createRunRoute, async (c) => {
const data = c.req.valid("json"); const data = c.req.valid("json");
const proto = c.req.headers.get('x-forwarded-proto') || "http"; const origin = new URL(c.req.url).origin;
const host = c.req.headers.get('x-forwarded-host') || c.req.headers.get('host');
const origin = `${proto}://${host}` || new URL(c.req.url).origin;
const apiKeyTokenData = c.get("apiKeyTokenData")!; const apiKeyTokenData = c.get("apiKeyTokenData")!;
const { deployment_id, inputs } = data; const { deployment_id, inputs } = data;
+1 -1
View File
@@ -102,7 +102,7 @@ export const createRun = withServerPromise(
let prompt_id: string | undefined = undefined; let prompt_id: string | undefined = undefined;
const shareData = { const shareData = {
workflow_api_raw: workflow_api, workflow_api: workflow_api,
status_endpoint: `${origin}/api/update-run`, status_endpoint: `${origin}/api/update-run`,
file_upload_endpoint: `${origin}/api/file-upload`, file_upload_endpoint: `${origin}/api/file-upload`,
}; };