Compare commits

..
5 Commits
Author SHA1 Message Date
webcoderz 4d53aa794a Update Dockerfile 2024-04-11 00:15:05 -04:00
webcoderz 12175b3955 remove apt deletion 2024-04-10 22:55:05 -04:00
webcoderz 2bcc71d24c various fixes and getting closer to parity with main 2024-04-10 20:38:24 -04:00
webcoderz 9484cb9b93 Update docker-compose.yaml
adding Postgres port env var
2024-04-10 18:17:49 -04:00
webcoderz a56ef1b06f adding local docker compose with local postgres 2024-03-28 11:15:46 -04:00
18 changed files with 575 additions and 1803 deletions
-21
View File
@@ -1,21 +0,0 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
-25
View File
@@ -1,25 +0,0 @@
class ComfyUIDeployExternalBoolean:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_bool"},
),
"default_value": ("BOOLEAN", {"default": False})
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("bool_value",)
FUNCTION = "run"
def run(self, input_id, default_value=None):
print(f"Node '{input_id}' processing with switch set to {default_value}")
return [default_value]
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalBoolean": ComfyUIDeployExternalBoolean}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalBoolean": "External Boolean (ComfyUI Deploy)"}
-85
View File
@@ -1,85 +0,0 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
import json
import comfy
class ComfyUIDeployExternalImageBatch:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_images"},
),
"images": (
"STRING",
{"multiline": False, "default": "[]"},
),
},
"optional": {
"default_value": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "run"
CATEGORY = "image"
def run(self, input_id, images=None, default_value=None):
processed_images = []
try:
images_list = json.loads(images) # Assuming images is a JSON array string
print(images_list)
for img_input in images_list:
if img_input.startswith('http'):
import requests
from io import BytesIO
print("Fetching image from url: ", img_input)
response = requests.get(img_input)
image = Image.open(BytesIO(response.content))
elif img_input.startswith('data:image/png;base64,') or img_input.startswith('data:image/jpeg;base64,') or img_input.startswith('data:image/jpg;base64,'):
import base64
from io import BytesIO
print("Decoding base64 image")
base64_image = img_input[img_input.find(",")+1:]
decoded_image = base64.b64decode(base64_image)
image = Image.open(BytesIO(decoded_image))
else:
raise ValueError("Invalid image url or base64 data provided.")
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,]
processed_images.append(image_tensor)
except Exception as e:
print(f"Error processing images: {e}")
pass
if default_value is not None and len(images_list) == 0:
processed_images.append(default_value) # Assuming default_value is a pre-processed image tensor
# Resize images if necessary and concatenate from MakeImageBatch in ImpactPack
if processed_images:
base_shape = processed_images[0].shape[1:] # Get the shape of the first image for comparison
batch_tensor = processed_images[0]
for i in range(1, len(processed_images)):
if processed_images[i].shape[1:] != base_shape:
# Resize to match the first image's dimensions
processed_images[i] = comfy.utils.common_upscale(processed_images[i].movedim(-1, 1), base_shape[1], base_shape[0], "lanczos", "center").movedim(1, -1)
batch_tensor = torch.cat((batch_tensor, processed_images[i]), dim=0)
# Concatenate using torch.cat
else:
batch_tensor = None # or handle the empty case as needed
return (batch_tensor, )
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalImageBatch": ComfyUIDeployExternalImageBatch}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalImageBatch": "External Image Batch (ComfyUI Deploy)"}
+7 -16
View File
@@ -16,8 +16,8 @@ class ComfyUIDeployExternalLora:
), ),
}, },
"optional": { "optional": {
"default_lora_name": (folder_paths.get_filename_list("loras"),), "default_lora_name": (folder_paths.get_filename_list("loras"), ),
}, }
} }
RETURN_TYPES = (folder_paths.get_filename_list("loras"),) RETURN_TYPES = (folder_paths.get_filename_list("loras"),)
@@ -32,29 +32,20 @@ class ComfyUIDeployExternalLora:
import os import os
import uuid import uuid
if default_lora_name.startswith("http"): if input_id and input_id.startswith('http'):
unique_filename = str(uuid.uuid4()) + ".safetensors" unique_filename = str(uuid.uuid4()) + ".safetensors"
print(unique_filename) print(unique_filename)
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], unique_filename)
folder_paths.folder_names_and_paths["loras"][0][0], unique_filename
)
print(destination_path) print(destination_path)
print("Downloading external lora - " + input_id + " to " + destination_path) print("Downloading external lora - " + input_id + " to " + destination_path)
response = requests.get( response = requests.get(input_id, headers={'User-Agent': 'Mozilla/5.0'}, allow_redirects=True)
input_id, with open(destination_path, 'wb') as out_file:
headers={"User-Agent": "Mozilla/5.0"},
allow_redirects=True,
)
with open(destination_path, "wb") as out_file:
out_file.write(response.content) out_file.write(response.content)
return (unique_filename,) return (unique_filename,)
else: else:
print(f"using lora: {default_lora_name}")
return (default_lora_name,) return (default_lora_name,)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalLora": ComfyUIDeployExternalLora} NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalLora": ComfyUIDeployExternalLora}
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalLora": "External Lora (ComfyUI Deploy)"}
"ComfyUIDeployExternalLora": "External Lora (ComfyUI Deploy)"
}
+1 -1
View File
@@ -29,7 +29,7 @@ class ComfyUIDeployExternalNumberInt:
CATEGORY = "number" CATEGORY = "number"
def run(self, input_id, default_value=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 not input_id.strip().isdigit():
return [default_value] return [default_value]
return [int(input_id)] return [int(input_id)]
-48
View File
@@ -1,48 +0,0 @@
class ComfyUIDeployExternalNumberSlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_number_slider"},
),
},
"optional": {
"default_value": (
"FLOAT",
{"multiline": True, "display": "number", "default": 0.5, "step": 0.01},
),
"min_value": (
"FLOAT",
{"multiline": True, "display": "number", "default": 0, "step": 0.01},
),
"max_value": (
"FLOAT",
{"multiline": True, "display": "number", "default": 1, "step": 0.01},
),
}
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("value",)
FUNCTION = "run"
CATEGORY = "number"
def run(self, input_id, default_value=None, min_value=0, max_value=1):
try:
float_value = float(input_id)
if min_value <= float_value <= max_value:
print("my number", float_value)
return [float_value]
else:
print("Number out of range. Returning default value:", default_value)
return [default_value]
except ValueError:
print("Invalid input. Returning default value:", default_value)
return [default_value]
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalNumberSlider": ComfyUIDeployExternalNumberSlider}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalNumberSlider": "External Number Slider (ComfyUI Deploy)"}
-78
View File
@@ -1,78 +0,0 @@
import os
import folder_paths
import uuid
from tqdm import tqdm
video_extensions = ["webm", "mp4", "mkv", "gif"]
class ComfyUIDeployExternalVideo:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = []
for f in os.listdir(input_dir):
if os.path.isfile(os.path.join(input_dir, f)):
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"},
),
},
"optional": {
"meta_batch": ("VHS_BatchManager",),
"default_value": (sorted(files),),
},
}
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video")
FUNCTION = "load_video"
def load_video(self, input_id, default_value):
input_dir = folder_paths.get_input_directory()
if input_id.startswith("http"):
import requests
print("Fetching video from URL: ", input_id)
response = requests.get(input_id, stream=True)
file_size = int(response.headers.get("Content-Length", 0))
file_extension = input_id.split(".")[-1].split("?")[
0
] # Extract extension and handle URLs with parameters
if file_extension not in video_extensions:
file_extension = ".mp4"
unique_filename = str(uuid.uuid4()) + "." + file_extension
video_path = os.path.join(input_dir, unique_filename)
chunk_size = 1024 # 1 Kibibyte
num_bars = int(file_size / chunk_size)
with open(video_path, "wb") as out_file:
for chunk in tqdm(
response.iter_content(chunk_size=chunk_size),
total=num_bars,
unit="KB",
desc="Downloading",
leave=True,
):
out_file.write(chunk)
else:
video_path = os.path.abspath(os.path.join(input_dir, default_value))
return (video_path,)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalVid": ComfyUIDeployExternalVideo}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyUIDeployExternalVid": "External Video (ComfyUI Deploy) path"
}
-594
View File
@@ -1,594 +0,0 @@
# credit goes to https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite and is meant to work with
import os
import itertools
import numpy as np
import torch
import cv2
import folder_paths
from comfy.utils import common_upscale
### Utils
import hashlib
from typing import Iterable
import shutil
import subprocess
import re
import uuid
import server
from tqdm import tqdm
BIGMIN = -(2**53 - 1)
BIGMAX = 2**53 - 1
DIMMAX = 8192
def ffmpeg_suitability(path):
try:
version = subprocess.run(
[path, "-version"], check=True, capture_output=True
).stdout.decode("utf-8")
except:
return 0
score = 0
# rough layout of the importance of various features
simple_criterion = [
("libvpx", 20),
("264", 10),
("265", 3),
("svtav1", 5),
("libopus", 1),
]
for criterion in simple_criterion:
if version.find(criterion[0]) >= 0:
score += criterion[1]
# obtain rough compile year from copyright information
copyright_index = version.find("2000-2")
if copyright_index >= 0:
copyright_year = version[copyright_index + 6 : copyright_index + 9]
if copyright_year.isnumeric():
score += int(copyright_year)
return score
if "VHS_FORCE_FFMPEG_PATH" in os.environ:
ffmpeg_path = os.environ.get("VHS_FORCE_FFMPEG_PATH")
else:
ffmpeg_paths = []
try:
from imageio_ffmpeg import get_ffmpeg_exe
imageio_ffmpeg_path = get_ffmpeg_exe()
ffmpeg_paths.append(imageio_ffmpeg_path)
except:
if "VHS_USE_IMAGEIO_FFMPEG" in os.environ:
raise
if "VHS_USE_IMAGEIO_FFMPEG" in os.environ:
ffmpeg_path = imageio_ffmpeg_path
else:
system_ffmpeg = shutil.which("ffmpeg")
if system_ffmpeg is not None:
ffmpeg_paths.append(system_ffmpeg)
if os.path.isfile("ffmpeg"):
ffmpeg_paths.append(os.path.abspath("ffmpeg"))
if os.path.isfile("ffmpeg.exe"):
ffmpeg_paths.append(os.path.abspath("ffmpeg.exe"))
if len(ffmpeg_paths) == 0:
ffmpeg_path = None
elif len(ffmpeg_paths) == 1:
# Evaluation of suitability isn't required, can take sole option
# to reduce startup time
ffmpeg_path = ffmpeg_paths[0]
else:
ffmpeg_path = max(ffmpeg_paths, key=ffmpeg_suitability)
gifski_path = os.environ.get("VHS_GIFSKI", None)
if gifski_path is None:
gifski_path = os.environ.get("JOV_GIFSKI", None)
if gifski_path is None:
gifski_path = shutil.which("gifski")
def get_sorted_dir_files_from_directory(
directory: str,
skip_first_images: int = 0,
select_every_nth: int = 1,
extensions: Iterable = None,
):
directory = directory.strip()
dir_files = os.listdir(directory)
dir_files = sorted(dir_files)
dir_files = [os.path.join(directory, x) for x in dir_files]
dir_files = list(filter(lambda filepath: os.path.isfile(filepath), dir_files))
# filter by extension, if needed
if extensions is not None:
extensions = list(extensions)
new_dir_files = []
for filepath in dir_files:
ext = "." + filepath.split(".")[-1]
if ext.lower() in extensions:
new_dir_files.append(filepath)
dir_files = new_dir_files
# start at skip_first_images
dir_files = dir_files[skip_first_images:]
dir_files = dir_files[0::select_every_nth]
return dir_files
# modified from https://stackoverflow.com/questions/22058048/hashing-a-file-in-python
def calculate_file_hash(filename: str, hash_every_n: int = 1):
# Larger video files were taking >.5 seconds to hash even when cached,
# so instead the modified time from the filesystem is used as a hash
h = hashlib.sha256()
h.update(filename.encode())
h.update(str(os.path.getmtime(filename)).encode())
return h.hexdigest()
prompt_queue = server.PromptServer.instance.prompt_queue
def requeue_workflow_unchecked():
"""Requeues the current workflow without checking for multiple requeues"""
currently_running = prompt_queue.currently_running
(_, _, prompt, extra_data, outputs_to_execute) = next(
iter(currently_running.values())
)
# Ensure batch_managers are marked stale
prompt = prompt.copy()
for uid in prompt:
if prompt[uid]["class_type"] == "VHS_BatchManager":
prompt[uid]["inputs"]["requeue"] = (
prompt[uid]["inputs"].get("requeue", 0) + 1
)
# execution.py has guards for concurrency, but server doesn't.
# TODO: Check that this won't be an issue
number = -server.PromptServer.instance.number
server.PromptServer.instance.number += 1
prompt_id = str(server.uuid.uuid4())
prompt_queue.put((number, prompt_id, prompt, extra_data, outputs_to_execute))
requeue_guard = [None, 0, 0, {}]
def requeue_workflow(requeue_required=(-1, True)):
assert len(prompt_queue.currently_running) == 1
global requeue_guard
(run_number, _, prompt, _, _) = next(iter(prompt_queue.currently_running.values()))
if requeue_guard[0] != run_number:
# Calculate a count of how many outputs are managed by a batch manager
managed_outputs = 0
for bm_uid in prompt:
if prompt[bm_uid]["class_type"] == "VHS_BatchManager":
for output_uid in prompt:
if prompt[output_uid]["class_type"] in ["VHS_VideoCombine"]:
for inp in prompt[output_uid]["inputs"].values():
if inp == [bm_uid, 0]:
managed_outputs += 1
requeue_guard = [run_number, 0, managed_outputs, {}]
requeue_guard[1] = requeue_guard[1] + 1
requeue_guard[3][requeue_required[0]] = requeue_required[1]
if requeue_guard[1] == requeue_guard[2] and max(requeue_guard[3].values()):
requeue_workflow_unchecked()
def get_audio(file, start_time=0, duration=0):
args = [ffmpeg_path, "-v", "error", "-i", file]
if start_time > 0:
args += ["-ss", str(start_time)]
if duration > 0:
args += ["-t", str(duration)]
try:
res = subprocess.run(
args + ["-f", "wav", "-"], stdout=subprocess.PIPE, check=True
).stdout
except subprocess.CalledProcessError as e:
return False
return res
def lazy_eval(func):
class Cache:
def __init__(self, func):
self.res = None
self.func = func
def get(self):
if self.res is None:
self.res = self.func()
return self.res
cache = Cache(func)
return lambda: cache.get()
def is_url(url):
return url.split("://")[0] in ["http", "https"]
def validate_sequence(path):
# Check if path is a valid ffmpeg sequence that points to at least one file
(path, file) = os.path.split(path)
if not os.path.isdir(path):
return False
match = re.search("%0?\d+d", file)
if not match:
return False
seq = match.group()
if seq == "%d":
seq = "\\\\d+"
else:
seq = "\\\\d{%s}" % seq[1:-1]
file_matcher = re.compile(re.sub("%0?\d+d", seq, file))
for file in os.listdir(path):
if file_matcher.fullmatch(file):
return True
return False
def hash_path(path):
if path is None:
return "input"
if is_url(path):
return "url"
return calculate_file_hash(path.strip('"'))
def validate_path(path, allow_none=False, allow_url=True):
if path is None:
return allow_none
if is_url(path):
# Probably not feasible to check if url resolves here
return True if allow_url else "URLs are unsupported for this path"
if not os.path.isfile(path.strip('"')):
return "Invalid file path: {}".format(path)
return True
### Utils
video_extensions = ["webm", "mp4", "mkv", "gif"]
def is_gif(filename) -> bool:
file_parts = filename.split(".")
return len(file_parts) > 1 and file_parts[-1] == "gif"
def target_size(
width, height, force_size, custom_width, custom_height
) -> tuple[int, int]:
if force_size == "Custom":
return (custom_width, custom_height)
elif force_size == "Custom Height":
force_size = "?x" + str(custom_height)
elif force_size == "Custom Width":
force_size = str(custom_width) + "x?"
if force_size != "Disabled":
force_size = force_size.split("x")
if force_size[0] == "?":
width = (width * int(force_size[1])) // height
# Limit to a multple of 8 for latent conversion
width = int(width) + 4 & ~7
height = int(force_size[1])
elif force_size[1] == "?":
height = (height * int(force_size[0])) // width
height = int(height) + 4 & ~7
width = int(force_size[0])
else:
width = int(force_size[0])
height = int(force_size[1])
return (width, height)
def cv_frame_generator(
video,
force_rate,
frame_load_cap,
skip_first_frames,
select_every_nth,
meta_batch=None,
unique_id=None,
):
video_cap = cv2.VideoCapture(video)
if not video_cap.isOpened():
raise ValueError(f"{video} could not be loaded with cv.")
# extract video metadata
fps = video_cap.get(cv2.CAP_PROP_FPS)
width = int(video_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
total_frames = int(video_cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / fps
# set video_cap to look at start_index frame
total_frame_count = 0
total_frames_evaluated = -1
frames_added = 0
base_frame_time = 1 / fps
prev_frame = None
if force_rate == 0:
target_frame_time = base_frame_time
else:
target_frame_time = 1 / force_rate
yield (width, height, fps, duration, total_frames, target_frame_time)
time_offset = target_frame_time - base_frame_time
while video_cap.isOpened():
if time_offset < target_frame_time:
is_returned = video_cap.grab()
# if didn't return frame, video has ended
if not is_returned:
break
time_offset += base_frame_time
if time_offset < target_frame_time:
continue
time_offset -= target_frame_time
# if not at start_index, skip doing anything with frame
total_frame_count += 1
if total_frame_count <= skip_first_frames:
continue
else:
total_frames_evaluated += 1
# if should not be selected, skip doing anything with frame
if total_frames_evaluated % select_every_nth != 0:
continue
# opencv loads images in BGR format (yuck), so need to convert to RGB for ComfyUI use
# follow up: can videos ever have an alpha channel?
# To my testing: No. opencv has no support for alpha
unused, frame = video_cap.retrieve()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# convert frame to comfyui's expected format
# TODO: frame contains no exif information. Check if opencv2 has already applied
frame = np.array(frame, dtype=np.float32) / 255.0
if prev_frame is not None:
inp = yield prev_frame
if inp is not None:
# ensure the finally block is called
return
prev_frame = frame
frames_added += 1
# if cap exists and we've reached it, stop processing frames
if frame_load_cap > 0 and frames_added >= frame_load_cap:
break
if meta_batch is not None:
meta_batch.inputs.pop(unique_id)
meta_batch.has_closed_inputs = True
if prev_frame is not None:
yield prev_frame
def load_video_cv(
video: str,
force_rate: int,
force_size: str,
custom_width: int,
custom_height: int,
frame_load_cap: int,
skip_first_frames: int,
select_every_nth: int,
meta_batch=None,
unique_id=None,
):
if meta_batch is None or unique_id not in meta_batch.inputs:
gen = cv_frame_generator(
video,
force_rate,
frame_load_cap,
skip_first_frames,
select_every_nth,
meta_batch,
unique_id,
)
(width, height, fps, duration, total_frames, target_frame_time) = next(gen)
if meta_batch is not None:
meta_batch.inputs[unique_id] = (
gen,
width,
height,
fps,
duration,
total_frames,
target_frame_time,
)
else:
(gen, width, height, fps, duration, total_frames, target_frame_time) = (
meta_batch.inputs[unique_id]
)
if meta_batch is not None:
gen = itertools.islice(gen, meta_batch.frames_per_batch)
# 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, (height, width, 3))))
)
if len(images) == 0:
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
audio = lambda: get_audio(
video,
skip_first_frames * target_frame_time,
frame_load_cap * target_frame_time * select_every_nth,
)
# Adjust target_frame_time for select_every_nth
target_frame_time *= select_every_nth
video_info = {
"source_fps": fps,
"source_frame_count": total_frames,
"source_duration": duration,
"source_width": width,
"source_height": height,
"loaded_fps": 1 / target_frame_time,
"loaded_frame_count": len(images),
"loaded_duration": len(images) * target_frame_time,
"loaded_width": images.shape[2],
"loaded_height": images.shape[1],
}
return (images, len(images), lazy_eval(audio), video_info)
class ComfyUIDeployExternalVideo:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = []
for f in os.listdir(input_dir):
if os.path.isfile(os.path.join(input_dir, f)):
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",),
"default_value": (sorted(files),),
},
"hidden": {"unique_id": "UNIQUE_ID"},
}
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
RETURN_TYPES = (
"IMAGE",
"INT",
"VHS_AUDIO",
"VHS_VIDEOINFO",
)
RETURN_NAMES = (
"IMAGE",
"frame_count",
"audio",
"video_info",
)
FUNCTION = "load_video"
def load_video(self, **kwargs):
input_id = kwargs.get("input_id")
force_rate = kwargs.get("force_rate")
force_size = kwargs.get("force_size", "Disabled")
custom_width = kwargs.get("custom_width")
custom_height = kwargs.get("custom_height")
frame_load_cap = kwargs.get("frame_load_cap")
skip_first_frames = kwargs.get("skip_first_frames")
select_every_nth = kwargs.get("select_every_nth")
meta_batch = kwargs.get("meta_batch")
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()
if input_id.startswith("http"):
import requests
print("Fetching video from URL: ", input_id)
response = requests.get(input_id, stream=True)
file_size = int(response.headers.get("Content-Length", 0))
file_extension = input_id.split(".")[-1].split("?")[
0
] # Extract extension and handle URLs with parameters
if file_extension not in video_extensions:
file_extension = ".mp4"
unique_filename = str(uuid.uuid4()) + "." + file_extension
video_path = os.path.join(input_dir, unique_filename)
chunk_size = 1024 # 1 Kibibyte
num_bars = int(file_size / chunk_size)
with open(video_path, "wb") as out_file:
for chunk in tqdm(
response.iter_content(chunk_size=chunk_size),
total=num_bars,
unit="KB",
desc="Downloading",
leave=True,
):
out_file.write(chunk)
print("video path: ", video_path)
return load_video_cv(
video=video_path,
force_rate=force_rate,
force_size=force_size,
custom_width=custom_width,
custom_height=custom_height,
frame_load_cap=frame_load_cap,
skip_first_frames=skip_first_frames,
select_every_nth=select_every_nth,
meta_batch=meta_batch,
unique_id=unique_id,
)
@classmethod
def IS_CHANGED(s, video, **kwargs):
image_path = folder_paths.get_annotated_filepath(video)
return calculate_file_hash(image_path)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalVideo": ComfyUIDeployExternalVideo}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyUIDeployExternalVideo": "External Video (ComfyUI Deploy x VHS)"
}
+136 -393
View File
@@ -13,71 +13,53 @@ import traceback
import uuid import uuid
import asyncio import asyncio
import logging import logging
from enum import Enum
from urllib.parse import quote from urllib.parse import quote
import threading import threading
import hashlib import hashlib
import aiohttp import aiohttp
import aiofiles import aiofiles
from typing import Dict, List, Union, Any, Optional from typing import List, Union, Any, Optional
from PIL import Image from PIL import Image
import copy import copy
import struct import struct
from logging import basicConfig, getLogger from globals import StreamingPrompt, sockets, streaming_prompt_metadata, BaseModel
import logfire
# if os.environ.get('LOGFIRE_TOKEN', None) is not None:
logfire.configure(
send_to_logfire="if-token-present"
)
# basicConfig(handlers=[logfire.LogfireLoggingHandler()])
logfire_handler = logfire.LogfireLoggingHandler()
logger = getLogger("comfy-deploy")
logger.addHandler(logfire_handler)
from globals import StreamingPrompt, Status, sockets, SimplePrompt, streaming_prompt_metadata, prompt_metadata class Status(Enum):
NOT_STARTED = "not-started"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
UPLOADING = "uploading"
class EventEmitter: class SimplePrompt(BaseModel):
def __init__(self): status_endpoint: str
self.listeners = {} file_upload_endpoint: str
workflow_api: dict
def on(self, event, listener): status: Status = Status.NOT_STARTED
if event not in self.listeners: progress: set = set()
self.listeners[event] = [] last_updated_node: Optional[str] = None,
self.listeners[event].append(listener) uploading_nodes: set = set()
done: bool = False
def off(self, event, listener): is_realtime: bool = False,
if event in self.listeners: start_time: Optional[float] = None,
self.listeners[event].remove(listener)
if not self.listeners[event]:
del self.listeners[event]
def emit(self, event, *args, **kwargs):
if event in self.listeners:
for listener in self.listeners[event]:
listener(*args, **kwargs)
# Create a global event emitter instance
event_emitter = EventEmitter()
api = None api = None
api_task = None api_task = None
prompt_metadata: dict[str, SimplePrompt] = {}
cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true' cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true'
cd_enable_run_log = os.environ.get('CD_ENABLE_RUN_LOG', 'false').lower() == 'true' cd_enable_run_log = os.environ.get('CD_ENABLE_RUN_LOG', 'false').lower() == 'true'
bypass_upload = os.environ.get('CD_BYPASS_UPLOAD', 'false').lower() == 'true'
logger.info(f"CD_BYPASS_UPLOAD {bypass_upload}")
def clear_current_prompt(sid): def clear_current_prompt(sid):
prompt_server = server.PromptServer.instance prompt_server = server.PromptServer.instance
to_delete = list(streaming_prompt_metadata[sid].running_prompt_ids) # Convert set to list to_delete = list(streaming_prompt_metadata[sid].running_prompt_ids) # Convert set to list
logger.info("clearning out prompt: ", to_delete) print("clearning out prompt: ", to_delete)
for id_to_delete in to_delete: for id_to_delete in to_delete:
delete_func = lambda a: a[1] == id_to_delete delete_func = lambda a: a[1] == id_to_delete
prompt_server.prompt_queue.delete_queue_item(delete_func) prompt_server.prompt_queue.delete_queue_item(delete_func)
logger.info("deleted prompt: ", id_to_delete, prompt_server.prompt_queue.get_tasks_remaining()) print("deleted prompt: ", id_to_delete, prompt_server.prompt_queue.get_tasks_remaining())
streaming_prompt_metadata[sid].running_prompt_ids.clear() streaming_prompt_metadata[sid].running_prompt_ids.clear()
@@ -118,7 +100,7 @@ def post_prompt(json_data):
} }
return response return response
else: else:
logger.info("invalid prompt:", valid[1]) print("invalid prompt:", valid[1])
return {"error": valid[1], "node_errors": valid[3]} return {"error": valid[1], "node_errors": valid[3]}
else: else:
return {"error": "no prompt", "node_errors": []} return {"error": "no prompt", "node_errors": []}
@@ -144,26 +126,30 @@ def apply_random_seed_to_workflow(workflow_api):
continue continue
workflow_api[key]['inputs']['seed'] = randomSeed(); workflow_api[key]['inputs']['seed'] = randomSeed();
def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None): def send_prompt(sid: str, inputs: StreamingPrompt):
# workflow_api = inputs.workflow_api
workflow_api = copy.deepcopy(inputs.workflow_api)
# Random seed
apply_random_seed_to_workflow(workflow_api)
print("getting inputs" , inputs.inputs)
# Loop through each of the inputs and replace them # Loop through each of the inputs and replace them
for key, value in workflow_api.items(): for key, value in workflow_api.items():
if 'inputs' in value: if 'inputs' in value:
if (value["class_type"] == "ComfyDeployWebscoketImageOutput"):
value['inputs']["client_id"] = sid
if (value["class_type"] == "ComfyDeployWebscoketImageInput"):
value['inputs']["client_id"] = sid
# Support websocket if "input_id" in value['inputs'] and value['inputs']['input_id'] in inputs.inputs:
if sid is not None: new_value = inputs.inputs[value['inputs']['input_id']]
if (value["class_type"] == "ComfyDeployWebscoketImageOutput"):
value['inputs']["client_id"] = sid
if (value["class_type"] == "ComfyDeployWebscoketImageInput"):
value['inputs']["client_id"] = sid
if "input_id" in value['inputs'] and inputs is not None and value['inputs']['input_id'] in inputs:
new_value = inputs[value['inputs']['input_id']]
# Lets skip it if its an image # Lets skip it if its an image
if isinstance(new_value, Image.Image): if isinstance(new_value, Image.Image):
continue continue
# Backward compactibility
value['inputs']["input_id"] = new_value value['inputs']["input_id"] = new_value
# Fix for external text default value # Fix for external text default value
@@ -173,30 +159,8 @@ def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
if (value["class_type"] == "ComfyUIDeployExternalCheckpoint"): if (value["class_type"] == "ComfyUIDeployExternalCheckpoint"):
value['inputs']["default_value"] = new_value value['inputs']["default_value"] = new_value
if (value["class_type"] == "ComfyUIDeployExternalImageBatch"):
value['inputs']["images"] = new_value
if value["class_type"] == "ComfyUIDeployExternalLora": print(workflow_api)
value["inputs"]["default_lora_name"] = new_value
if value["class_type"] == "ComfyUIDeployExternalSlider":
value["inputs"]["default_value"] = new_value
if value["class_type"] == "ComfyUIDeployExternalBoolean":
value["inputs"]["default_value"] = new_value
def send_prompt(sid: str, inputs: StreamingPrompt):
# workflow_api = inputs.workflow_api
workflow_api = copy.deepcopy(inputs.workflow_api)
# Random seed
apply_random_seed_to_workflow(workflow_api)
logger.info("getting inputs" , inputs.inputs)
apply_inputs_to_workflow(workflow_api, inputs.inputs, sid=sid)
logger.info(workflow_api)
prompt_id = str(uuid.uuid4()) prompt_id = str(uuid.uuid4())
@@ -219,22 +183,23 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
error_type = type(e).__name__ error_type = type(e).__name__
stack_trace_short = traceback.format_exc().strip().split('\n')[-2] stack_trace_short = traceback.format_exc().strip().split('\n')[-2]
stack_trace = traceback.format_exc().strip() stack_trace = traceback.format_exc().strip()
logger.info(f"error: {error_type}, {e}") print(f"error: {error_type}, {e}")
logger.info(f"stack trace: {stack_trace_short}") print(f"stack trace: {stack_trace_short}")
@server.PromptServer.instance.routes.post("/comfyui-deploy/run") @server.PromptServer.instance.routes.post("/comfyui-deploy/run")
async def comfy_deploy_run(request): async def comfy_deploy_run(request):
prompt_server = server.PromptServer.instance
data = await request.json() data = await request.json()
# In older version, we use workflow_api, but this has inputs already swapped in nextjs frontend, which is tricky workflow_api = data.get("workflow_api")
workflow_api = data.get("workflow_api_raw")
# The prompt id generated from comfy deploy, can be None # The prompt id generated from comfy deploy, can be None
prompt_id = data.get("prompt_id") prompt_id = data.get("prompt_id")
inputs = data.get("inputs")
# Now it handles directly in here
apply_random_seed_to_workflow(workflow_api) apply_random_seed_to_workflow(workflow_api)
apply_inputs_to_workflow(workflow_api, inputs) # for key in workflow_api:
# if 'inputs' in workflow_api[key] and 'seed' in workflow_api[key]['inputs']:
# workflow_api[key]['inputs']['seed'] = randomSeed()
prompt = { prompt = {
"prompt": workflow_api, "prompt": workflow_api,
@@ -254,8 +219,8 @@ async def comfy_deploy_run(request):
error_type = type(e).__name__ error_type = type(e).__name__
stack_trace_short = traceback.format_exc().strip().split('\n')[-2] stack_trace_short = traceback.format_exc().strip().split('\n')[-2]
stack_trace = traceback.format_exc().strip() stack_trace = traceback.format_exc().strip()
logger.info(f"error: {error_type}, {e}") print(f"error: {error_type}, {e}")
logger.info(f"stack trace: {stack_trace_short}") print(f"stack trace: {stack_trace_short}")
await update_run_with_output(prompt_id, { await update_run_with_output(prompt_id, {
"error": { "error": {
"error_type": error_type, "error_type": error_type,
@@ -267,6 +232,13 @@ async def comfy_deploy_run(request):
return web.Response(status=500, reason=f"{error_type}: {e}, {stack_trace_short}") return web.Response(status=500, reason=f"{error_type}: {e}, {stack_trace_short}")
status = 200 status = 200
# if "error" in res:
# status = 400
# await update_run_with_output(prompt_id, {
# "error": {
# **res
# }
# })
if "node_errors" in res and res["node_errors"]: if "node_errors" in res and res["node_errors"]:
# Even tho there are node_errors it can still be run # Even tho there are node_errors it can still be run
@@ -283,134 +255,24 @@ async def comfy_deploy_run(request):
return web.json_response(res, status=status) return web.json_response(res, status=status)
async def stream_prompt(data):
# In older version, we use workflow_api, but this has inputs already swapped in nextjs frontend, which is tricky
workflow_api = data.get("workflow_api_raw")
# The prompt id generated from comfy deploy, can be None
prompt_id = data.get("prompt_id")
inputs = data.get("inputs")
# Now it handles directly in here
apply_random_seed_to_workflow(workflow_api)
apply_inputs_to_workflow(workflow_api, inputs)
prompt = {
"prompt": workflow_api,
"client_id": "comfy_deploy_instance", #api.client_id
"prompt_id": prompt_id
}
prompt_metadata[prompt_id] = SimplePrompt(
status_endpoint=data.get('status_endpoint'),
file_upload_endpoint=data.get('file_upload_endpoint'),
workflow_api=workflow_api
)
logfire.info("Begin prompt", prompt=prompt)
try:
res = post_prompt(prompt)
except Exception as e:
error_type = type(e).__name__
stack_trace_short = traceback.format_exc().strip().split('\n')[-2]
stack_trace = traceback.format_exc().strip()
logger.info(f"error: {error_type}, {e}")
logger.info(f"stack trace: {stack_trace_short}")
await update_run_with_output(prompt_id, {
"error": {
"error_type": error_type,
"stack_trace": stack_trace
}
})
# When there are critical errors, the prompt is actually not run
await update_run(prompt_id, Status.FAILED)
# return web.Response(status=500, reason=f"{error_type}: {e}, {stack_trace_short}")
# raise Exception("Prompt failed")
status = 200
if "node_errors" in res and res["node_errors"]:
# Even tho there are node_errors it can still be run
status = 400
await update_run_with_output(prompt_id, {
"error": {
**res
}
})
# When there are critical errors, the prompt is actually not run
if "error" in res:
await update_run(prompt_id, Status.FAILED)
# raise Exception("Prompt failed")
return res
# return web.json_response(res, status=status)
comfy_message_queues: Dict[str, asyncio.Queue] = {}
@server.PromptServer.instance.routes.post('/comfyui-deploy/run/streaming')
async def stream_response(request):
response = web.StreamResponse(status=200, reason='OK', headers={'Content-Type': 'text/event-stream'})
await response.prepare(request)
pending = True
data = await request.json()
prompt_id = data.get("prompt_id")
comfy_message_queues[prompt_id] = asyncio.Queue()
with logfire.span('Streaming Run'):
logfire.info('Streaming prompt')
try:
result = await stream_prompt(data=data)
await response.write(f"event: event_update\ndata: {json.dumps(result)}\n\n".encode('utf-8'))
# await response.write(.encode('utf-8'))
await response.drain() # Ensure the buffer is flushed
while pending:
if prompt_id in comfy_message_queues:
if not comfy_message_queues[prompt_id].empty():
data = await comfy_message_queues[prompt_id].get()
logfire.info(data["event"], data=json.dumps(data))
# logger.info("listener", data)
await response.write(f"event: event_update\ndata: {json.dumps(data)}\n\n".encode('utf-8'))
await response.drain() # Ensure the buffer is flushed
if data["event"] == "status":
if data["data"]["status"] in (Status.FAILED.value, Status.SUCCESS.value):
pending = False
await asyncio.sleep(0.1) # Adjust the sleep duration as needed
except asyncio.CancelledError:
logfire.info("Streaming was cancelled")
raise
except Exception as e:
logfire.error("Streaming error", error=e)
finally:
# event_emitter.off("send_json", task)
await response.write_eof()
comfy_message_queues.pop(prompt_id, None)
return response
def get_comfyui_path_from_file_path(file_path): def get_comfyui_path_from_file_path(file_path):
file_path_parts = file_path.split("\\") file_path_parts = file_path.split("\\")
if file_path_parts[0] == "input": if file_path_parts[0] == "input":
logger.info("matching input") print("matching input")
file_path = os.path.join(folder_paths.get_directory_by_type("input"), *file_path_parts[1:]) file_path = os.path.join(folder_paths.get_directory_by_type("input"), *file_path_parts[1:])
elif file_path_parts[0] == "models": elif file_path_parts[0] == "models":
logger.info("matching models") print("matching models")
file_path = folder_paths.get_full_path(file_path_parts[1], os.path.join(*file_path_parts[2:])) file_path = folder_paths.get_full_path(file_path_parts[1], os.path.join(*file_path_parts[2:]))
logger.info(file_path) print(file_path)
return file_path return file_path
# Form ComfyUI Manager # Form ComfyUI Manager
async def compute_sha256_checksum(filepath): async def compute_sha256_checksum(filepath):
logger.info("computing sha256 checksum") print("computing sha256 checksum")
chunk_size = 1024 * 256 # Example: 256KB chunk_size = 1024 * 256 # Example: 256KB
filepath = get_comfyui_path_from_file_path(filepath) filepath = get_comfyui_path_from_file_path(filepath)
"""Compute the SHA256 checksum of a file, in chunks, asynchronously""" """Compute the SHA256 checksum of a file, in chunks, asynchronously"""
@@ -433,7 +295,7 @@ async def get_installed_models(request):
file_list = folder_paths.get_filename_list(key) file_list = folder_paths.get_filename_list(key)
value_json_compatible = (value[0], list(value[1]), file_list) value_json_compatible = (value[0], list(value[1]), file_list)
new_dict[key] = value_json_compatible new_dict[key] = value_json_compatible
# logger.info(new_dict) # print(new_dict)
return web.json_response(new_dict) return web.json_response(new_dict)
# This is start uploading the files to Comfy Deploy # This is start uploading the files to Comfy Deploy
@@ -443,7 +305,7 @@ async def upload_file_endpoint(request):
file_path = data.get("file_path") file_path = data.get("file_path")
logger.info("Original file path", file_path) print("Original file path", file_path)
file_path = get_comfyui_path_from_file_path(file_path) file_path = get_comfyui_path_from_file_path(file_path)
@@ -493,7 +355,7 @@ async def upload_file_endpoint(request):
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
headers = { headers = {
"Content-Type": file_type, "Content-Type": file_type,
# "x-amz-acl": "public-read", "x-amz-acl": "public-read",
"Content-Length": str(file_size) "Content-Length": str(file_size)
} }
async with session.put(upload_url, data=f, headers=headers) as upload_response: async with session.put(upload_url, data=f, headers=headers) as upload_response:
@@ -520,58 +382,26 @@ async def upload_file_endpoint(request):
}, status=500) }, status=500)
script_dir = os.path.dirname(os.path.abspath(__file__))
# Assuming the cache file is stored in the same directory as this script
CACHE_FILE_PATH = script_dir + '/file-hash-cache.json'
# Global in-memory cache
file_hash_cache = {}
# Load cache from disk at startup
def load_cache():
global file_hash_cache
try:
with open(CACHE_FILE_PATH, 'r') as cache_file:
file_hash_cache = json.load(cache_file)
except (FileNotFoundError, json.JSONDecodeError):
file_hash_cache = {}
# Save cache to disk
def save_cache():
with open(CACHE_FILE_PATH, 'w') as cache_file:
json.dump(file_hash_cache, cache_file)
# Initialize cache on application start
load_cache()
@server.PromptServer.instance.routes.get('/comfyui-deploy/get-file-hash') @server.PromptServer.instance.routes.get('/comfyui-deploy/get-file-hash')
async def get_file_hash(request): async def get_file_hash(request):
file_path = request.rel_url.query.get('file_path', '') file_path = request.rel_url.query.get('file_path', '')
if not file_path: if file_path is None:
return web.json_response({ return web.json_response({
"error": "file_path is required" "error": "file_path is required"
}, status=400) }, status=400)
try: try:
base = folder_paths.base_path base = folder_paths.base_path
full_file_path = os.path.join(base, file_path) file_path = os.path.join(base, file_path)
# print("file_path", file_path)
# Check if the file hash is in the cache start_time = time.time() # Capture the start time
if full_file_path in file_hash_cache: file_hash = await compute_sha256_checksum(
file_hash = file_hash_cache[full_file_path] file_path
else: )
start_time = time.time() end_time = time.time() # Capture the end time after the code execution
file_hash = await compute_sha256_checksum(full_file_path) elapsed_time = end_time - start_time # Calculate the elapsed time
end_time = time.time() print(f"Execution time: {elapsed_time} seconds")
elapsed_time = end_time - start_time
logger.info(f"Cache miss -> Execution time: {elapsed_time} seconds")
# Update the in-memory cache
file_hash_cache[full_file_path] = file_hash
save_cache()
return web.json_response({ return web.json_response({
"file_hash": file_hash "file_hash": file_hash
}) })
@@ -585,8 +415,6 @@ async def update_realtime_run_status(realtime_id: str, status_endpoint: str, sta
"run_id": realtime_id, "run_id": realtime_id,
"status": status.value, "status": status.value,
} }
if (status_endpoint is None):
return
# requests.post(status_endpoint, json=body) # requests.post(status_endpoint, json=body)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(status_endpoint, json=body) as response: async with session.post(status_endpoint, json=body) as response:
@@ -617,7 +445,7 @@ async def websocket_handler(request):
if response.status == 200: if response.status == 200:
workflow = await response.json() workflow = await response.json()
logger.info(f"Loaded workflow version ${workflow['version']}") print("Loaded workflow version ",workflow["version"])
streaming_prompt_metadata[sid] = StreamingPrompt( streaming_prompt_metadata[sid] = StreamingPrompt(
workflow_api=workflow["workflow_api"], workflow_api=workflow["workflow_api"],
@@ -631,7 +459,7 @@ async def websocket_handler(request):
# await send("workflow_api", workflow_api, sid) # await send("workflow_api", workflow_api, sid)
else: else:
error_message = await response.text() error_message = await response.text()
logger.info(f"Failed to fetch workflow endpoint. Status: {response.status}, Error: {error_message}") print(f"Failed to fetch workflow endpoint. Status: {response.status}, Error: {error_message}")
# await send("error", {"message": error_message}, sid) # await send("error", {"message": error_message}, sid)
try: try:
@@ -646,10 +474,10 @@ async def websocket_handler(request):
if msg.type == aiohttp.WSMsgType.TEXT: if msg.type == aiohttp.WSMsgType.TEXT:
try: try:
data = json.loads(msg.data) data = json.loads(msg.data)
logger.info(data) print(data)
event_type = data.get('event') event_type = data.get('event')
if event_type == 'input': if event_type == 'input':
logger.info(f"Got input: ${data.get('inputs')}") print("Got input: ", data.get("inputs"))
input = data.get('inputs') input = data.get('inputs')
streaming_prompt_metadata[sid].inputs.update(input) streaming_prompt_metadata[sid].inputs.update(input)
elif event_type == 'queue_prompt': elif event_type == 'queue_prompt':
@@ -659,7 +487,7 @@ async def websocket_handler(request):
# Handle other event types # Handle other event types
pass pass
except json.JSONDecodeError: except json.JSONDecodeError:
logger.info('Failed to decode JSON from message') print('Failed to decode JSON from message')
if msg.type == aiohttp.WSMsgType.BINARY: if msg.type == aiohttp.WSMsgType.BINARY:
data = msg.data data = msg.data
@@ -668,9 +496,9 @@ async def websocket_handler(request):
image_type_code, = struct.unpack("<I", data[4:8]) image_type_code, = struct.unpack("<I", data[4:8])
input_id_bytes = data[8:32] # Extract the next 24 bytes for the input ID input_id_bytes = data[8:32] # Extract the next 24 bytes for the input ID
input_id = input_id_bytes.decode('ascii').strip() # Decode the input ID from ASCII input_id = input_id_bytes.decode('ascii').strip() # Decode the input ID from ASCII
logger.info(event_type) print(event_type)
logger.info(image_type_code) print(image_type_code)
logger.info(input_id) print(input_id)
image_data = data[32:] # The rest is the image data image_data = data[32:] # The rest is the image data
if image_type_code == 1: if image_type_code == 1:
image_type = "JPEG" image_type = "JPEG"
@@ -679,7 +507,7 @@ async def websocket_handler(request):
elif image_type_code == 3: elif image_type_code == 3:
image_type = "WEBP" image_type = "WEBP"
else: else:
logger.info(f"Unknown image type code: ${image_type_code}") print("Unknown image type code:", image_type_code)
return return
image = Image.open(BytesIO(image_data)) image = Image.open(BytesIO(image_data))
# Check if the input ID already exists and replace the input with the new one # Check if the input ID already exists and replace the input with the new one
@@ -690,14 +518,14 @@ async def websocket_handler(request):
if hasattr(existing_image, 'close'): if hasattr(existing_image, 'close'):
existing_image.close() existing_image.close()
except Exception as e: except Exception as e:
logger.info(f"Error closing previous image for input ID {input_id}: {e}") print(f"Error closing previous image for input ID {input_id}: {e}")
streaming_prompt_metadata[sid].inputs[input_id] = image streaming_prompt_metadata[sid].inputs[input_id] = image
# clear_current_prompt(sid) # clear_current_prompt(sid)
# send_prompt(sid, streaming_prompt_metadata[sid]) # send_prompt(sid, streaming_prompt_metadata[sid])
logger.info(f"Received {image_type} image of size {image.size} with input ID {input_id}") print(f"Received {image_type} image of size {image.size} with input ID {input_id}")
if msg.type == aiohttp.WSMsgType.ERROR: if msg.type == aiohttp.WSMsgType.ERROR:
logger.info('ws connection closed with exception %s' % ws.exception()) print('ws connection closed with exception %s' % ws.exception())
finally: finally:
sockets.pop(sid, None) sockets.pop(sid, None)
@@ -742,16 +570,16 @@ async def send(event, data, sid=None):
if not ws.closed: # Check if the WebSocket connection is open and not closing if not ws.closed: # Check if the WebSocket connection is open and not closing
await ws.send_json({ 'event': event, 'data': data }) await ws.send_json({ 'event': event, 'data': data })
except Exception as e: except Exception as e:
logger.info(f"Exception: {e}") print(f"Exception: {e}")
traceback.print_exc() traceback.print_exc()
logging.basicConfig(level=logging.INFO)
prompt_server = server.PromptServer.instance prompt_server = server.PromptServer.instance
send_json = prompt_server.send_json send_json = prompt_server.send_json
async def send_json_override(self, event, data, sid=None): async def send_json_override(self, event, data, sid=None):
# logger.info("INTERNAL:", event, data, sid) # print("INTERNAL:", event, data, sid)
prompt_id = data.get('prompt_id') prompt_id = data.get('prompt_id')
target_sid = sid target_sid = sid
@@ -764,19 +592,8 @@ async def send_json_override(self, event, data, sid=None):
asyncio.create_task(self.send_json_original(event, data, sid)) asyncio.create_task(self.send_json_original(event, data, sid))
]) ])
if prompt_id in comfy_message_queues:
comfy_message_queues[prompt_id].put_nowait({
"event": event,
"data": data
})
# event_emitter.emit("send_json", {
# "event": event,
# "data": data
# })
if event == 'execution_start': if event == 'execution_start':
await update_run(prompt_id, Status.RUNNING) update_run(prompt_id, Status.RUNNING)
if prompt_id in prompt_metadata: if prompt_id in prompt_metadata:
prompt_metadata[prompt_id].start_time = time.perf_counter() prompt_metadata[prompt_id].start_time = time.perf_counter()
@@ -785,12 +602,12 @@ async def send_json_override(self, event, data, sid=None):
if event == 'executing' and data.get('node') is None: if event == 'executing' and data.get('node') is None:
mark_prompt_done(prompt_id=prompt_id) mark_prompt_done(prompt_id=prompt_id)
if not have_pending_upload(prompt_id): if not have_pending_upload(prompt_id):
await update_run(prompt_id, Status.SUCCESS) update_run(prompt_id, Status.SUCCESS)
if prompt_id in prompt_metadata: if prompt_id in prompt_metadata:
current_time = time.perf_counter() current_time = time.perf_counter()
if prompt_metadata[prompt_id].start_time is not None: if prompt_metadata[prompt_id].start_time is not None:
elapsed_time = current_time - prompt_metadata[prompt_id].start_time elapsed_time = current_time - prompt_metadata[prompt_id].start_time
logger.info(f"Elapsed time: {elapsed_time} seconds") print(f"Elapsed time: {elapsed_time} seconds")
await send("elapsed_time", { await send("elapsed_time", {
"prompt_id": prompt_id, "prompt_id": prompt_id,
"elapsed_time": elapsed_time "elapsed_time": elapsed_time
@@ -805,13 +622,13 @@ async def send_json_override(self, event, data, sid=None):
prompt_metadata[prompt_id].progress.add(node) prompt_metadata[prompt_id].progress.add(node)
calculated_progress = len(prompt_metadata[prompt_id].progress) / len(prompt_metadata[prompt_id].workflow_api) calculated_progress = len(prompt_metadata[prompt_id].progress) / len(prompt_metadata[prompt_id].workflow_api)
# logger.info("calculated_progress", calculated_progress) # print("calculated_progress", calculated_progress)
if prompt_metadata[prompt_id].last_updated_node is not None and prompt_metadata[prompt_id].last_updated_node == node: if prompt_metadata[prompt_id].last_updated_node is not None and prompt_metadata[prompt_id].last_updated_node == node:
return return
prompt_metadata[prompt_id].last_updated_node = node prompt_metadata[prompt_id].last_updated_node = node
class_type = prompt_metadata[prompt_id].workflow_api[node]['class_type'] class_type = prompt_metadata[prompt_id].workflow_api[node]['class_type']
logger.info(f"updating run live status {class_type}") print("updating run live status", class_type)
await send("live_status", { await send("live_status", {
"prompt_id": prompt_id, "prompt_id": prompt_id,
"current_node": class_type, "current_node": class_type,
@@ -832,19 +649,10 @@ async def send_json_override(self, event, data, sid=None):
if event == 'execution_error': if event == 'execution_error':
# Careful this might not be fully awaited. # Careful this might not be fully awaited.
await update_run_with_output(prompt_id, data) await update_run_with_output(prompt_id, data)
await update_run(prompt_id, Status.FAILED) update_run(prompt_id, Status.FAILED)
# await update_run_with_output(prompt_id, data) # await update_run_with_output(prompt_id, data)
if event == 'executed' and 'node' in data and 'output' in data: if event == 'executed' and 'node' in data and 'output' in data:
logger.info(f"executed {data}")
if prompt_id in prompt_metadata:
node = data.get('node')
class_type = prompt_metadata[prompt_id].workflow_api[node]['class_type']
logger.info(f"executed {class_type}")
if class_type == "PreviewImage":
logger.info("skipping preview image")
return
await update_run_with_output(prompt_id, data.get('output'), node_id=data.get('node')) await update_run_with_output(prompt_id, data.get('output'), node_id=data.get('node'))
# await update_run_with_output(prompt_id, data.get('output'), node_id=data.get('node')) # await update_run_with_output(prompt_id, data.get('output'), node_id=data.get('node'))
# update_run_with_output(prompt_id, data.get('output')) # update_run_with_output(prompt_id, data.get('output'))
@@ -859,36 +667,21 @@ async def update_run_live_status(prompt_id, live_status, calculated_progress: fl
if prompt_metadata[prompt_id].is_realtime is True: if prompt_metadata[prompt_id].is_realtime is True:
return return
print("progress", calculated_progress)
status_endpoint = prompt_metadata[prompt_id].status_endpoint status_endpoint = prompt_metadata[prompt_id].status_endpoint
if (status_endpoint is None):
return
logger.info(f"progress {calculated_progress}")
body = { body = {
"run_id": prompt_id, "run_id": prompt_id,
"live_status": live_status, "live_status": live_status,
"progress": calculated_progress "progress": calculated_progress
} }
if prompt_id in comfy_message_queues:
comfy_message_queues[prompt_id].put_nowait({
"event": "live_status",
"data": {
"prompt_id": prompt_id,
"live_status": live_status,
"progress": calculated_progress
}
})
# requests.post(status_endpoint, json=body) # requests.post(status_endpoint, json=body)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(status_endpoint, json=body) as response: async with session.post(status_endpoint, json=body) as response:
pass pass
async def update_run(prompt_id: str, status: Status): def update_run(prompt_id: str, status: Status):
global last_read_line_number global last_read_line_number
if prompt_id not in prompt_metadata: if prompt_id not in prompt_metadata:
@@ -903,7 +696,7 @@ async def update_run(prompt_id: str, status: Status):
if (prompt_metadata[prompt_id].status != status): if (prompt_metadata[prompt_id].status != status):
# when the status is already failed, we don't want to update it to success # when the status is already failed, we don't want to update it to success
if (prompt_metadata[prompt_id].status is Status.FAILED): if ('status' in prompt_metadata[prompt_id] and prompt_metadata[prompt_id].status == Status.FAILED):
return return
status_endpoint = prompt_metadata[prompt_id].status_endpoint status_endpoint = prompt_metadata[prompt_id].status_endpoint
@@ -911,22 +704,18 @@ async def update_run(prompt_id: str, status: Status):
"run_id": prompt_id, "run_id": prompt_id,
"status": status.value, "status": status.value,
} }
logger.info(f"Status: {status.value}") print(f"Status: {status.value}")
try: try:
# requests.post(status_endpoint, json=body) requests.post(status_endpoint, json=body)
if (status_endpoint is not None):
async with aiohttp.ClientSession() as session:
async with session.post(status_endpoint, json=body) as response:
pass
if (status_endpoint is not None) and cd_enable_run_log and (status == Status.SUCCESS or status == Status.FAILED): if cd_enable_run_log and (status == Status.SUCCESS or status == Status.FAILED):
try: try:
with open(comfyui_file_path, 'r') as log_file: with open(comfyui_file_path, 'r') as log_file:
# log_data = log_file.read() # log_data = log_file.read()
# Move to the last read line # Move to the last read line
all_log_data = log_file.read() # Read all log data all_log_data = log_file.read() # Read all log data
# logger.info("All log data before skipping: ") # Log all data before skipping print("All log data before skipping:", all_log_data) # Log all data before skipping
log_file.seek(0) # Reset file pointer to the beginning log_file.seek(0) # Reset file pointer to the beginning
for _ in range(last_read_line_number): for _ in range(last_read_line_number):
@@ -934,9 +723,9 @@ async def update_run(prompt_id: str, status: Status):
log_data = log_file.read() log_data = log_file.read()
# Update the last read line number # Update the last read line number
last_read_line_number += log_data.count('\n') last_read_line_number += log_data.count('\n')
# logger.info("last_read_line_number", last_read_line_number) print("last_read_line_number", last_read_line_number)
# logger.info("log_data", log_data) print("log_data", log_data)
# logger.info("log_data.count(n)", log_data.count('\n')) print("log_data.count(n)", log_data.count('\n'))
body = { body = {
"run_id": prompt_id, "run_id": prompt_id,
@@ -947,28 +736,16 @@ async def update_run(prompt_id: str, status: Status):
} }
] ]
} }
requests.post(status_endpoint, json=body)
async with aiohttp.ClientSession() as session:
async with session.post(status_endpoint, json=body) as response:
pass
# requests.post(status_endpoint, json=body)
except Exception as log_error: except Exception as log_error:
logger.info(f"Error reading log file: {log_error}") print(f"Error reading log file: {log_error}")
except Exception as e: except Exception as e:
error_type = type(e).__name__ error_type = type(e).__name__
stack_trace = traceback.format_exc().strip() stack_trace = traceback.format_exc().strip()
logger.info(f"Error occurred while updating run: {e} {stack_trace}") print(f"Error occurred while updating run: {e} {stack_trace}")
finally: finally:
prompt_metadata[prompt_id].status = status prompt_metadata[prompt_id].status = status
if prompt_id in comfy_message_queues:
comfy_message_queues[prompt_id].put_nowait({
"event": "status",
"data": {
"prompt_id": prompt_id,
"status": status.value,
}
})
async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output"): async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output"):
@@ -986,7 +763,7 @@ async def upload_file(prompt_id, filename, subfolder=None, content_type="image/p
output_dir = folder_paths.get_directory_by_type(type) output_dir = folder_paths.get_directory_by_type(type)
if output_dir is None: if output_dir is None:
logger.info(f"{filename} Upload failed: output_dir is None") print(filename, "Upload failed: output_dir is None")
return return
if subfolder != None: if subfolder != None:
@@ -998,7 +775,7 @@ async def upload_file(prompt_id, filename, subfolder=None, content_type="image/p
filename = os.path.basename(filename) filename = os.path.basename(filename)
file = os.path.join(output_dir, filename) file = os.path.join(output_dir, filename)
logger.info(f"uploading file {file}") print("uploading file", file)
file_upload_endpoint = prompt_metadata[prompt_id].file_upload_endpoint file_upload_endpoint = prompt_metadata[prompt_id].file_upload_endpoint
@@ -1008,34 +785,27 @@ async def upload_file(prompt_id, filename, subfolder=None, content_type="image/p
target_url = f"{file_upload_endpoint}?file_name={filename}&run_id={prompt_id}&type={content_type}" target_url = f"{file_upload_endpoint}?file_name={filename}&run_id={prompt_id}&type={content_type}"
start_time = time.time() # Start timing here
result = requests.get(target_url) result = requests.get(target_url)
end_time = time.time() # End timing after the request is complete
logger.info("Time taken for getting file upload endpoint: {:.2f} seconds".format(end_time - start_time))
ok = result.json() ok = result.json()
start_time = time.time() # Start timing here
with open(file, 'rb') as f: with open(file, 'rb') as f:
data = f.read() data = f.read()
headers = { headers = {
# "x-amz-acl": "public-read", "x-amz-acl": "public-read",
"Content-Type": content_type, "Content-Type": content_type,
"Content-Length": str(len(data)), "Content-Length": str(len(data)),
} }
# response = requests.put(ok.get("url"), headers=headers, data=data) response = requests.put(ok.get("url"), headers=headers, data=data)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.put(ok.get("url"), headers=headers, data=data) as response: async with session.put(ok.get("url"), headers=headers, data=data) as response:
logger.info(f"Upload file response status: {response.status}, status text: {response.reason}") print("upload file response", response.status)
end_time = time.time() # End timing after the request is complete
logger.info("Upload time: {:.2f} seconds".format(end_time - start_time))
def have_pending_upload(prompt_id): def have_pending_upload(prompt_id):
if prompt_id in prompt_metadata and len(prompt_metadata[prompt_id].uploading_nodes) > 0: if prompt_id in prompt_metadata and len(prompt_metadata[prompt_id].uploading_nodes) > 0:
logger.info(f"have pending upload {len(prompt_metadata[prompt_id].uploading_nodes)}") print("have pending upload ", len(prompt_metadata[prompt_id].uploading_nodes))
return True return True
logger.info("no pending upload") print("no pending upload")
return False return False
def mark_prompt_done(prompt_id): def mark_prompt_done(prompt_id):
@@ -1047,7 +817,7 @@ def mark_prompt_done(prompt_id):
""" """
if prompt_id in prompt_metadata: if prompt_id in prompt_metadata:
prompt_metadata[prompt_id].done = True prompt_metadata[prompt_id].done = True
logger.info("Prompt done") print("Prompt done")
def is_prompt_done(prompt_id: str): def is_prompt_done(prompt_id: str):
""" """
@@ -1079,8 +849,8 @@ async def handle_error(prompt_id, data, e: Exception):
} }
} }
await update_file_status(prompt_id, data, False, have_error=True) await update_file_status(prompt_id, data, False, have_error=True)
logger.info(body) print(body)
logger.info(f"Error occurred while uploading file: {e}") print(f"Error occurred while uploading file: {e}")
# Mark the current prompt requires upload, and block it from being marked as success # Mark the current prompt requires upload, and block it from being marked as success
async def update_file_status(prompt_id: str, data, uploading, have_error=False, node_id=None): async def update_file_status(prompt_id: str, data, uploading, have_error=False, node_id=None):
@@ -1093,11 +863,11 @@ async def update_file_status(prompt_id: str, data, uploading, have_error=False,
else: else:
prompt_metadata[prompt_id].uploading_nodes.discard(node_id) prompt_metadata[prompt_id].uploading_nodes.discard(node_id)
logger.info(prompt_metadata[prompt_id].uploading_nodes) print(prompt_metadata[prompt_id].uploading_nodes)
# Update the remote status # Update the remote status
if have_error: if have_error:
await update_run(prompt_id, Status.FAILED) update_run(prompt_id, Status.FAILED)
await send("failed", { await send("failed", {
"prompt_id": prompt_id, "prompt_id": prompt_id,
}) })
@@ -1106,15 +876,15 @@ async def update_file_status(prompt_id: str, data, uploading, have_error=False,
# if there are still nodes that are uploading, then we set the status to uploading # if there are still nodes that are uploading, then we set the status to uploading
if uploading: if uploading:
if prompt_metadata[prompt_id].status != Status.UPLOADING: if prompt_metadata[prompt_id].status != Status.UPLOADING:
await update_run(prompt_id, Status.UPLOADING) update_run(prompt_id, Status.UPLOADING)
await send("uploading", { await send("uploading", {
"prompt_id": prompt_id, "prompt_id": prompt_id,
}) })
# if there are no nodes that are uploading, then we set the status to success # if there are no nodes that are uploading, then we set the status to success
elif not uploading and not have_pending_upload(prompt_id) and is_prompt_done(prompt_id=prompt_id): elif not uploading and not have_pending_upload(prompt_id) and is_prompt_done(prompt_id=prompt_id):
await update_run(prompt_id, Status.SUCCESS) update_run(prompt_id, Status.SUCCESS)
# logger.info("Status: SUCCUSS") print("Status: SUCCUSS")
await send("success", { await send("success", {
"prompt_id": prompt_id, "prompt_id": prompt_id,
}) })
@@ -1122,27 +892,15 @@ async def update_file_status(prompt_id: str, data, uploading, have_error=False,
async def handle_upload(prompt_id: str, data, key: str, content_type_key: str, default_content_type: str): async def handle_upload(prompt_id: str, data, key: str, content_type_key: str, default_content_type: str):
items = data.get(key, []) items = data.get(key, [])
for item in items: for item in items:
# # Skipping temp files
if item.get("type") == "temp":
continue
file_type = item.get(content_type_key, default_content_type)
file_extension = os.path.splitext(item.get("filename"))[1]
if file_extension in ['.jpg', '.jpeg']:
file_type = 'image/jpeg'
elif file_extension == '.png':
file_type = 'image/png'
elif file_extension == '.webp':
file_type = 'image/webp'
await upload_file( await upload_file(
prompt_id, prompt_id,
item.get("filename"), item.get("filename"),
subfolder=item.get("subfolder"), subfolder=item.get("subfolder"),
type=item.get("type"), type=item.get("type"),
content_type=file_type content_type=item.get(content_type_key, default_content_type)
) )
# Upload files in the background # Upload files in the background
async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=True): async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=True):
try: try:
@@ -1150,7 +908,6 @@ async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=T
await handle_upload(prompt_id, data, 'files', "content_type", "image/png") await handle_upload(prompt_id, data, 'files', "content_type", "image/png")
# This will also be mp4 # This will also be mp4
await handle_upload(prompt_id, data, 'gifs', "format", "image/gif") await handle_upload(prompt_id, data, 'gifs', "format", "image/gif")
await handle_upload(prompt_id, data, 'mesh', "format", "application/octet-stream")
if have_upload: if have_upload:
await update_file_status(prompt_id, data, False, node_id=node_id) await update_file_status(prompt_id, data, False, node_id=node_id)
@@ -1170,29 +927,21 @@ async def update_run_with_output(prompt_id, data, node_id=None):
"run_id": prompt_id, "run_id": prompt_id,
"output_data": data "output_data": data
} }
have_upload_media = 'images' in data or 'files' in data or 'gifs' in data or 'mesh' in data
if bypass_upload and have_upload_media:
print("CD_BYPASS_UPLOAD is enabled, skipping the upload of the output:", node_id)
return
if have_upload_media: try:
try: have_upload = 'images' in data or 'files' in data or 'gifs' in data
logger.info(f"\nhave_upload {have_upload} {node_id}") print("\nhave_upload", have_upload, node_id)
if have_upload_media: if have_upload:
await update_file_status(prompt_id, data, True, node_id=node_id) await update_file_status(prompt_id, data, True, node_id=node_id)
asyncio.create_task(upload_in_background(prompt_id, data, node_id=node_id, have_upload=have_upload_media)) asyncio.create_task(upload_in_background(prompt_id, data, node_id=node_id, have_upload=have_upload))
# await upload_in_background(prompt_id, data, node_id=node_id, have_upload=have_upload)
except Exception as e: except Exception as e:
await handle_error(prompt_id, data, e) await handle_error(prompt_id, data, e)
# requests.post(status_endpoint, json=body)
if status_endpoint is not None: requests.post(status_endpoint, json=body)
async with aiohttp.ClientSession() as session:
async with session.post(status_endpoint, json=body) as response:
pass
await send('outputs_uploaded', { await send('outputs_uploaded', {
"prompt_id": prompt_id "prompt_id": prompt_id
@@ -1246,9 +995,3 @@ def run_in_new_thread(coroutine):
if cd_enable_log: if cd_enable_log:
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket)) run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
# use after calling GET /object_info (it populates the `filename_list_cache` variable)
@server.PromptServer.instance.routes.get("/comfyui-deploy/filename_list_cache")
async def get_filename_list_cache(_):
from folder_paths import filename_list_cache
return web.json_response({'filename_list': filename_list_cache})
+55
View File
@@ -0,0 +1,55 @@
version: '3.9'
services:
comfy-deploy:
build:
context: .
dockerfile: ./local/Dockerfile
restart: unless-stopped
volumes:
- ./local/scripts/entrypoint.sh:/comfyui-deploy/web/deploy_entrypoint.sh
entrypoint: /comfyui-deploy/web/deploy_entrypoint.sh
ports:
- 3000:3000
depends_on:
- postgres
- pg_proxy
- localstack
environment:
VSCODE_DEV_CONTAINER: true
### comfy-deploy services
postgres:
image: "postgres:15.2-alpine"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: verceldb
POSTGRES_PORT: 5480
expose:
- 5480
pg_proxy:
image: ghcr.io/neondatabase/wsproxy:latest
environment:
APPEND_PORT: "postgres:5480"
ALLOW_ADDR_REGEX: ".*"
LOG_TRAFFIC: "true"
expose:
- 80
depends_on:
- postgres
localstack:
image: localstack/localstack:latest
environment:
SERVICES: s3
ports:
- 4566:4566
volumes:
- ../localstack/aws:/etc/localstack/init/ready.d
- ../localstack/aws:/app/web/aws
+5 -24
View File
@@ -1,45 +1,26 @@
import struct import struct
from enum import Enum
import aiohttp import aiohttp
from typing import List, Union, Any, Optional from typing import List, Union, Any, Optional
from PIL import Image, ImageOps 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):
NOT_STARTED = "not-started"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
UPLOADING = "uploading"
class StreamingPrompt(BaseModel): class StreamingPrompt(BaseModel):
workflow_api: Any workflow_api: Any
auth_token: str auth_token: str
inputs: dict[str, Union[str, bytes, Image.Image]] inputs: dict[str, Union[str, bytes, Image.Image]]
running_prompt_ids: set[str] = set() running_prompt_ids: set[str] = set()
status_endpoint: Optional[str] status_endpoint: str
file_upload_endpoint: Optional[str] file_upload_endpoint: str
class SimplePrompt(BaseModel):
status_endpoint: Optional[str]
file_upload_endpoint: Optional[str]
workflow_api: dict
status: Status = Status.NOT_STARTED
progress: set = set()
last_updated_node: Optional[str] = None,
uploading_nodes: set = set()
done: bool = False
is_realtime: bool = False,
start_time: Optional[float] = None,
sockets = dict() sockets = dict()
prompt_metadata: dict[str, SimplePrompt] = {}
streaming_prompt_metadata: dict[str, StreamingPrompt] = {} streaming_prompt_metadata: dict[str, StreamingPrompt] = {}
class BinaryEventTypes: class BinaryEventTypes:
+18
View File
@@ -0,0 +1,18 @@
FROM node:21-bullseye AS comfy_deploy
RUN apt-get update && apt-get install -y python3 make g++
RUN npm install -g bun
COPY ./web /web
WORKDIR /web
RUN cp .env.example .env.local
RUN bunx node-gyp
RUN bun i
ENTRYPOINT [ "bun", "dev" ]
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
echo "comfy deploy container starting.."
echo "Running migrations.."
bun migrate-local
echo "Starting comfy deploy.."
bun dev
-6
View File
@@ -58,9 +58,6 @@ if cd_enable_log:
print("** Comfy Deploy logging enabled") print("** Comfy Deploy logging enabled")
setup() setup()
# Store the original working directory
original_cwd = os.getcwd()
try: try:
# Get the absolute path of the script's directory # Get the absolute path of the script's directory
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -70,6 +67,3 @@ try:
print(f"** Comfy Deploy Revision: {current_git_commit}") print(f"** Comfy Deploy Revision: {current_git_commit}")
except Exception as e: except Exception as e:
print(f"** Comfy Deploy failed to get current git commit: {str(e)}") print(f"** Comfy Deploy failed to get current git commit: {str(e)}")
finally:
# Change back to the original directory
os.chdir(original_cwd)
-15
View File
@@ -1,15 +0,0 @@
[project]
name = "comfyui-deploy"
description = "Open source comfyui deployment platform, a vercel for generative workflow infra."
version = "1.0.0"
license = "LICENSE"
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg"]
[project.urls]
Repository = "https://github.com/BennyKok/comfyui-deploy"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "comfydeploy"
DisplayName = "comfyui-deploy"
Icon = ""
-3
View File
@@ -1,5 +1,2 @@
aiofiles aiofiles
pydantic pydantic
opencv-python
imageio-ffmpeg
logfire
+284 -434
View File
@@ -1,90 +1,10 @@
import { app } from "./app.js"; 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]5"; import { generateDependencyGraph } from "https://esm.sh/[email protected]2";
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>`;
function sendEventToCD(event, data) {
const message = {
type: event,
data: data,
};
window.parent.postMessage(JSON.stringify(message), "*");
}
function dispatchAPIEventData(data) {
const msg = JSON.parse(data);
// Custom parse error
if (msg.error) {
let message = msg.error.message;
if (msg.error.details)
message += ": " + msg.error.details;
for (const [nodeID, nodeError] of Object.entries(
msg.node_errors,
)) {
message += "\n" + nodeError.class_type + ":";
for (const errorReason of nodeError.errors) {
message +=
"\n - " + errorReason.message + ": " + errorReason.details;
}
}
app.ui.dialog.show(message);
if (msg.node_errors) {
app.lastNodeErrors = msg.node_errors;
app.canvas.draw(true, true);
}
}
switch (msg.event) {
case "error":
break;
case "status":
if (msg.data.sid) {
// this.clientId = msg.data.sid;
// window.name = this.clientId; // use window name so it isnt reused when duplicating tabs
// sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow
}
api.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
break;
case "progress":
api.dispatchEvent(new CustomEvent("progress", { detail: msg.data }));
break;
case "executing":
api.dispatchEvent(
new CustomEvent("executing", { detail: msg.data.node }),
);
break;
case "executed":
api.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
break;
case "execution_start":
api.dispatchEvent(
new CustomEvent("execution_start", { detail: msg.data }),
);
break;
case "execution_error":
api.dispatchEvent(
new CustomEvent("execution_error", { detail: msg.data }),
);
break;
case "execution_cached":
api.dispatchEvent(
new CustomEvent("execution_cached", { detail: msg.data }),
);
break;
default:
api.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
// default:
// if (this.#registered.has(msg.type)) {
// } else {
// throw new Error(`Unknown message type ${msg.type}`);
// }
}
}
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ /** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
/** @type {ComfyExtension} */ /** @type {ComfyExtension} */
const ext = { const ext = {
@@ -98,34 +18,6 @@ const ext = {
const auth_token = queryParams.get("auth_token"); const auth_token = queryParams.get("auth_token");
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");
if (workspace_mode) {
document.querySelector(".comfy-menu").style.display = "none";
sendEventToCD("cd_plugin_onInit");
app.queuePrompt = ((originalFunction) => async () => {
// const prompt = await app.graphToPrompt();
sendEventToCD("cd_plugin_onQueuePromptTrigger");
})(app.queuePrompt);
// // Intercept the onkeydown event
// window.addEventListener(
// "keydown",
// (event) => {
// // Check for specific keys if necessary
// console.log("hi");
// if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
// event.preventDefault();
// event.stopImmediatePropagation();
// event.stopPropagation();
// sendEventToCD("cd_plugin_onQueuePrompt", prompt);
// }
// },
// true,
// );
}
const data = getData(); const data = getData();
let endpoint = data.endpoint; let endpoint = data.endpoint;
@@ -260,37 +152,9 @@ const ext = {
async setup() { async setup() {
// const graphCanvas = document.getElementById("graph-canvas"); // const graphCanvas = document.getElementById("graph-canvas");
window.addEventListener("message", async (event) => { window.addEventListener("message", (event) => {
try { if (!event.data.flow || Object.entries(event.data.flow).length <= 0)
const message = JSON.parse(event.data); return;
if (message.type === "graph_load") {
const comfyUIWorkflow = message.data;
console.log("recieved: ", comfyUIWorkflow);
// 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
// For demonstration, let's assume there's a loadWorkflow method in the ComfyUI API
if (comfyUIWorkflow && app && app.loadGraphData) {
app.loadGraphData(comfyUIWorkflow);
}
} else if (message.type === "deploy") {
// deployWorkflow();
const prompt = await app.graphToPrompt();
sendEventToCD("cd_plugin_onDeployChanges", prompt);
} else if (message.type === "queue_prompt") {
const prompt = await app.graphToPrompt();
sendEventToCD("cd_plugin_onQueuePrompt", prompt);
} else if (message.type === "event") {
dispatchAPIEventData(message.data);
}
// else if (message.type === "refresh") {
// sendEventToCD("cd_plugin_onRefresh");
// }
} catch (error) {
// console.error("Error processing message:", error);
}
// if (!event.data.flow || Object.entries(event.data.flow).length <= 0)
// return;
// updateBlendshapesPrompts(event.data.flow); // updateBlendshapesPrompts(event.data.flow);
}); });
@@ -303,18 +167,6 @@ const ext = {
// } // }
}); });
app.graph.onAfterChange = ((originalFunction) =>
async function () {
const prompt = await app.graphToPrompt();
sendEventToCD("cd_plugin_onAfterChange", prompt);
if (typeof originalFunction === "function") {
originalFunction.apply(this, arguments);
}
})(app.graph.onAfterChange);
sendEventToCD("cd_plugin_setup");
}, },
}; };
@@ -415,296 +267,294 @@ function createDynamicUIHtml(data) {
return html; return html;
} }
async function deployWorkflow() {
const deploy = document.getElementById("deploy-button");
/** @type {LGraph} */
const graph = app.graph;
let { endpoint, apiKey, displayName } = getData();
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
configDialog.show();
return;
}
let deployMeta = graph.findNodesByType("ComfyDeploy");
if (deployMeta.length == 0) {
const text = await inputDialog.input(
"Create your deployment",
"Workflow name",
);
if (!text) return;
console.log(text);
app.graph.beforeChange();
var node = LiteGraph.createNode("ComfyDeploy");
node.configure({
widgets_values: [text],
});
node.pos = [0, 0];
app.graph.add(node);
app.graph.afterChange();
deployMeta = [node];
}
const deployMetaNode = deployMeta[0];
const workflow_name = deployMetaNode.widgets[0].value;
const workflow_id = deployMetaNode.widgets[1].value;
const ok = await confirmDialog.confirm(
`Confirm deployment`,
`
<div>
A new version of <button style="font-size: 18px;">${workflow_name}</button> will be deployed, do you confirm?
<br><br>
<button style="font-size: 18px;">${displayName}</button>
<br>
<button style="font-size: 18px;">${endpoint}</button>
<br><br>
<label>
<input id="include-deps" type="checkbox" checked>Include dependency</input>
</label>
<br>
<label>
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label>
</div>
`,
);
if (!ok) return;
const includeDeps = document.getElementById("include-deps").checked;
const reuseHash = document.getElementById("reuse-hash").checked;
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
loadingDialog.showLoading("Generating snapshot");
const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
// console.log(snapshot);
loadingDialog.close();
if (!snapshot) {
showError(
"Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager",
);
return;
}
const title = deploy.querySelector("#button-title");
const prompt = await app.graphToPrompt();
let deps = undefined;
if (includeDeps) {
loadingDialog.showLoading("Fetching existing version");
const existing_workflow = await fetch(
endpoint + "/api/workflow/" + workflow_id,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
},
)
.then((x) => x.json())
.catch(() => {
return {};
});
loadingDialog.close();
loadingDialog.showLoading("Generating dependency graph");
deps = await generateDependencyGraph({
workflow_api: prompt.output,
snapshot: snapshot,
computeFileHash: async (file) => {
console.log(existing_workflow?.dependencies?.models);
// Match previous hash for models
if (reuseHash && existing_workflow?.dependencies?.models) {
const previousModelHash = Object.entries(
existing_workflow?.dependencies?.models,
).flatMap(([key, value]) => {
return Object.values(value).map((x) => ({
...x,
name: "models/" + key + "/" + x.name,
}));
});
console.log(previousModelHash);
const match = previousModelHash.find((x) => {
console.log(file, x.name);
return file == x.name;
});
console.log(match);
if (match && match.hash) {
console.log("cached hash used");
return match.hash;
}
}
console.log(file);
loadingDialog.showLoading("Generating hash", file);
const hash = await fetch(
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(file)}`,
).then((x) => x.json());
loadingDialog.showLoading("Generating hash", file);
console.log(hash);
return hash.file_hash;
},
handleFileUpload: async (file, hash, prevhash) => {
console.log("Uploading ", file);
loadingDialog.showLoading("Uploading file", file);
try {
const { download_url } = await fetch(`/comfyui-deploy/upload-file`, {
method: "POST",
body: JSON.stringify({
file_path: file,
token: apiKey,
url: endpoint + "/api/upload-url",
}),
})
.then((x) => x.json())
.catch(() => {
loadingDialog.close();
confirmDialog.confirm("Error", "Unable to upload file " + file);
});
loadingDialog.showLoading("Uploaded file", file);
console.log(download_url);
return download_url;
} catch (error) {
return undefined;
}
},
existingDependencies: existing_workflow.dependencies,
});
// Need to find a way to include this if this is not included in comfyui-json level
if (
!deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy"] &&
!deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy.git"]
)
deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy"] = {
url: "https://github.com/BennyKok/comfyui-deploy",
install_type: "git-clone",
hash:
snapshot?.git_custom_nodes?.[
"https://github.com/BennyKok/comfyui-deploy"
]?.hash ?? "HEAD",
name: "ComfyUI Deploy",
};
loadingDialog.close();
const depsOk = await confirmDialog.confirm(
"Check dependencies",
// JSON.stringify(deps, null, 2),
`
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
<iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
JSON.stringify(deps),
)}" />`,
// createDynamicUIHtml(deps),
);
if (!depsOk) return;
console.log(deps);
}
loadingDialog.showLoading("Deploying...");
title.innerText = "Deploying...";
title.style.color = "orange";
// console.log(prompt);
// TODO trim the ending / from endpoint is there is
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
// console.log(prompt.workflow);
const apiRoute = endpoint + "/api/workflow";
// const userId = apiKey
try {
const body = {
workflow_name,
workflow_id,
workflow: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
dependencies: deps,
};
console.log(body);
let data = await fetch(apiRoute, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
});
console.log(data);
if (data.status !== 200) {
throw new Error(await data.text());
} else {
data = await data.json();
}
loadingDialog.close();
title.textContent = "Done";
title.style.color = "green";
deployMetaNode.widgets[1].value = data.workflow_id;
deployMetaNode.widgets[2].value = data.version;
graph.change();
infoDialog.show(
`<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/>`,
);
setTimeout(() => {
title.textContent = "Deploy";
title.style.color = "white";
}, 1000);
} catch (e) {
loadingDialog.close();
app.ui.dialog.show(e);
console.error(e);
title.textContent = "Error";
title.style.color = "red";
setTimeout(() => {
title.textContent = "Deploy";
title.style.color = "white";
}, 1000);
}
}
function addButton() { function addButton() {
const menu = document.querySelector(".comfy-menu"); const menu = document.querySelector(".comfy-menu");
const deploy = document.createElement("button"); const deploy = document.createElement("button");
deploy.id = "deploy-button";
deploy.style.position = "relative"; deploy.style.position = "relative";
deploy.style.display = "block"; deploy.style.display = "block";
deploy.innerHTML = "<div id='button-title'>Deploy</div>"; deploy.innerHTML = "<div id='button-title'>Deploy</div>";
deploy.onclick = async () => { deploy.onclick = async () => {
await deployWorkflow(); /** @type {LGraph} */
const graph = app.graph;
let { endpoint, apiKey, displayName } = getData();
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
configDialog.show();
return;
}
let deployMeta = graph.findNodesByType("ComfyDeploy");
if (deployMeta.length == 0) {
const text = await inputDialog.input(
"Create your deployment",
"Workflow name",
);
if (!text) return;
console.log(text);
app.graph.beforeChange();
var node = LiteGraph.createNode("ComfyDeploy");
node.configure({
widgets_values: [text],
});
node.pos = [0, 0];
app.graph.add(node);
app.graph.afterChange();
deployMeta = [node];
}
const deployMetaNode = deployMeta[0];
const workflow_name = deployMetaNode.widgets[0].value;
const workflow_id = deployMetaNode.widgets[1].value;
const ok = await confirmDialog.confirm(
`Confirm deployment`,
`
<div>
A new version of <button style="font-size: 18px;">${workflow_name}</button> will be deployed, do you confirm?
<br><br>
<button style="font-size: 18px;">${displayName}</button>
<br>
<button style="font-size: 18px;">${endpoint}</button>
<br><br>
<label>
<input id="include-deps" type="checkbox" checked>Include dependency</input>
</label>
<br>
<label>
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label>
</div>
`,
);
if (!ok) return;
const includeDeps = document.getElementById("include-deps").checked;
const reuseHash = document.getElementById("reuse-hash").checked;
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
loadingDialog.showLoading("Generating snapshot");
const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
// console.log(snapshot);
loadingDialog.close();
if (!snapshot) {
showError(
"Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager",
);
return;
}
const title = deploy.querySelector("#button-title");
const prompt = await app.graphToPrompt();
let deps = undefined;
if (includeDeps) {
loadingDialog.showLoading("Fetching existing version");
const existing_workflow = await fetch(
endpoint + "/api/workflow/" + workflow_id,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
},
)
.then((x) => x.json())
.catch(() => {
return {};
});
loadingDialog.close();
loadingDialog.showLoading("Generating dependency graph");
deps = await generateDependencyGraph({
workflow_api: prompt.output,
snapshot: snapshot,
computeFileHash: async (file) => {
console.log(existing_workflow?.dependencies?.models);
// Match previous hash for models
if (reuseHash && existing_workflow?.dependencies?.models) {
const previousModelHash = Object.entries(
existing_workflow?.dependencies?.models,
).flatMap(([key, value]) => {
return Object.values(value).map((x) => ({
...x,
name: "models/" + key + "/" + x.name,
}));
});
console.log(previousModelHash);
const match = previousModelHash.find((x) => {
console.log(file, x.name);
return file == x.name;
});
console.log(match);
if (match && match.hash) {
console.log("cached hash used");
return match.hash;
}
}
console.log(file);
loadingDialog.showLoading("Generating hash", file);
const hash = await fetch(
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(
file,
)}`,
).then((x) => x.json());
loadingDialog.showLoading("Generating hash", file);
console.log(hash);
return hash.file_hash;
},
handleFileUpload: async (file, hash, prevhash) => {
console.log("Uploading ", file);
loadingDialog.showLoading("Uploading file", file);
try {
const { download_url } = await fetch(
`/comfyui-deploy/upload-file`,
{
method: "POST",
body: JSON.stringify({
file_path: file,
token: apiKey,
url: endpoint + "/api/upload-url",
}),
},
)
.then((x) => x.json())
.catch(() => {
loadingDialog.close();
confirmDialog.confirm("Error", "Unable to upload file " + file);
});
loadingDialog.showLoading("Uploaded file", file);
console.log(download_url);
return download_url;
} catch (error) {
return undefined;
}
},
existingDependencies: existing_workflow.dependencies,
});
// Need to find a way to include this if this is not included in comfyui-json level
if (
!deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy"] &&
!deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy.git"]
)
deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy"] = {
url: "https://github.com/BennyKok/comfyui-deploy",
install_type: "git-clone",
hash:
snapshot?.git_custom_nodes?.[
"https://github.com/BennyKok/comfyui-deploy"
]?.hash ?? "HEAD",
name: "ComfyUI Deploy",
};
loadingDialog.close();
const depsOk = await confirmDialog.confirm(
"Check dependencies",
// JSON.stringify(deps, null, 2),
`
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
<iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
JSON.stringify(deps),
)}" />`,
// createDynamicUIHtml(deps),
);
if (!depsOk) return;
console.log(deps);
}
loadingDialog.showLoading("Deploying...");
title.innerText = "Deploying...";
title.style.color = "orange";
// console.log(prompt);
// TODO trim the ending / from endpoint is there is
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
// console.log(prompt.workflow);
const apiRoute = endpoint + "/api/workflow";
// const userId = apiKey
try {
const body = {
workflow_name,
workflow_id,
workflow: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
dependencies: deps,
};
console.log(body);
let data = await fetch(apiRoute, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
});
console.log(data);
if (data.status !== 200) {
throw new Error(await data.text());
} else {
data = await data.json();
}
loadingDialog.close();
title.textContent = "Done";
title.style.color = "green";
deployMetaNode.widgets[1].value = data.workflow_id;
deployMetaNode.widgets[2].value = data.version;
graph.change();
infoDialog.show(
`<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/>`,
);
setTimeout(() => {
title.textContent = "Deploy";
title.style.color = "white";
}, 1000);
} catch (e) {
loadingDialog.close();
app.ui.dialog.show(e);
console.error(e);
title.textContent = "Error";
title.style.color = "red";
setTimeout(() => {
title.textContent = "Deploy";
title.style.color = "white";
}, 1000);
}
}; };
const config = document.createElement("img"); const config = document.createElement("img");
+2 -2
View File
@@ -9,10 +9,10 @@ if (process.env.VERCEL_ENV !== "production") {
// Set the WebSocket proxy to work with the local instance // Set the WebSocket proxy to work with the local instance
if (isDevContainer) { if (isDevContainer) {
// Running inside a VS Code devcontainer // Running inside a VS Code devcontainer
neonConfig.wsProxy = (host) => "host.docker.internal:5481/v1"; neonConfig.wsProxy = (host) => "pg_proxy:80/v1";
} else { } else {
// Not running inside a VS Code devcontainer // Not running inside a VS Code devcontainer
neonConfig.wsProxy = (host) => `${host}:5481/v1`; neonConfig.wsProxy = (host) => "pg_proxy:80/v1";
} }
// Disable all authentication and encryption // Disable all authentication and encryption
neonConfig.useSecureWebSocket = false; neonConfig.useSecureWebSocket = false;