Compare commits

...
Author SHA1 Message Date
EmmanuelMr18 d97994a66e fix(upload outputs): skip images/gifs/files/mesh when env var is true
The env var is `CD_BYPASS_UPLOAD`.
When that variables is `True`, we don't upload the media to our comfy
deploy s3 bucket.

There are 2 steps.
1. save the file into our s3 bucket
2. save the saving into our database.

When `CD_BYPASS_UPLOAD` is True:
1. Skip the save file into our s3 bucket
2. Skip the save into our database

Previously we were skipping the step 1, but not the step 2. So that is
the reason of why we keep seeing the comfy deploy URL when fetching the
run details:

```
outputs: [
  {
    data:{
      gifs: [
        {
          url: "https://comfy-deploy-output.s3.amazonaws.com/video.mp4"
        }
      ],
      text: [
        "A text that you displayed with show text node"
      ]
    }
  }
]
```

With the new changes we don't save that into our database, and fetching
the details of a run will look like this:
```
outputs: [
  {
    data:{
      text: [
        "A text that you displayed with show text node"
      ]
    }
  }
]
```
2024-07-07 19:16:40 -06:00
EmmanuelMr18 e70a9c5e9e Revert "fix(image upload): skip when using the CD_BYPASS_UPLOAD env var"
This reverts commit 384eda63e6.
2024-07-07 18:52:21 -06:00
EmmanuelMr18 384eda63e6 fix(image upload): skip when using the CD_BYPASS_UPLOAD env var 2024-07-06 13:02:30 -06:00
nick c6fe88bf66 new route 2024-06-15 17:29:51 -07:00
bennykok 9b24b12006 fix: file upload issues with cloudflare 2024-06-11 17:42:52 -07:00
bennykok ff70bbdcec fix: correctly set the file content type for images, webp, jepg, png 2024-05-29 08:59:53 -07:00
haohaocreates 840bea79e8 chore(publish): Add Github Action for Publishing to Comfy Registry (#48) 2024-05-26 23:25:15 +08:00
BennyKok 0f423ce1c3 Update pyproject.toml 2024-05-26 23:21:13 +08:00
haohaocreates 2aa1a446e5 chore(pyproject): Add pyproject.toml for Custom Node Registry (#47) 2024-05-26 23:20:50 +08:00
karrix 07a7feb6ac add: slider number support 2024-05-11 14:50:46 +08:00
bennykok c5ac1b5f94 perf: turn back on async file upload 2024-05-10 13:08:37 +09:00
bennykok 00d827e232 feat: CD_BYPASS_UPLOAD 2024-05-10 11:36:00 +09:00
karrix 697fd52349 add: bool custom node 2024-05-09 14:26:43 +08:00
karrix 6b9c431df8 add: boolean input and 3d mesh support 2024-05-09 14:25:22 +08:00
bennykok 3c508c7eec feat: redirect queue prompt to iframe event in workspace mode 2024-05-07 00:42:36 +08:00
Nick Kao 409ca6f1dd Merge pull request #45 from NicholasKao1029/main
video node
2024-05-04 10:19:07 -07:00
nick df391e867e video node 2024-05-04 10:14:33 -07:00
Nick Kao c37b8be00a Merge pull request #44 from NicholasKao1029/main
Video node
2024-04-30 12:56:30 -07:00
nick a5a73e4209 clean up 2024-04-30 12:55:04 -07:00
nick c7841deea2 vid node 2024-04-30 12:19:41 -07:00
nick b0b1d64b6b external video 2024-04-27 13:32:50 -07:00
bennykok c8dc189f99 fix: external number input 2024-04-25 18:36:24 +08:00
bennykok cd5e4a5d01 fix: duplicated file upload 2024-04-25 16:14:14 +08:00
bennykok 95c15f095d chore: add file upload time log 2024-04-25 15:55:34 +08:00
nick b4c27bbbea fix: external lora 2024-04-24 23:27:01 -07:00
bennykok 810aec5135 fix: empty inputs causing run issues 2024-04-25 13:15:55 +08:00
nick c843926d6e fix: external lora takes in value outside of default 2024-04-24 17:35:09 -07:00
bennykok 797180b5c7 feat(plugin): add external image batch 2024-04-24 21:48:56 +08:00
bennykok d00ca375a2 chore: bump comfyui json version 2024-04-23 18:44:29 +08:00
bennykok be5d5d2b54 feat: update deploy method 2024-04-23 14:11:11 +08:00
bennykok d592a6ba12 feat: refactor deployment code 2024-04-22 00:07:26 +08:00
bennykok 35fed9aa4d fix: failed case marked as success 2024-04-20 01:38:31 +08:00
bennykok 3b6a753472 feat: workspace_mode and window event 2024-04-19 16:01:47 +08:00
bennykok 7d2c521645 chore: clean up custom node log 2024-04-14 15:59:33 +08:00
bennykok f363b7e871 fix: make sure to skip the temp file. 2024-04-14 00:24:21 +08:00
bennykok 1b25cfdd6c feat: add file hash cache, workflow deployment will be faster
# Conflicts:
#	.gitignore
2024-04-12 19:53:03 +08:00
bennykok 5da56b5507 chore: tweak log 2024-04-12 18:43:24 +08:00
bennykok 03d12e4099 fix!: skipping preview image as save node 2024-04-12 13:34:27 +08:00
bennykok e66712425d fix: bump comfydeploy deps 2024-04-12 12:28:41 +08:00
bennykok 81f315e14d fix: clashes with ComfyUI manager restart 2024-03-27 13:14:57 -07:00
bennykok 7189f13263 fix: added queue_prompt from event, now input and image will not trigger queue prompt 2024-03-18 14:31:43 -07:00
bennykok e73392ba8b fix(plugin): external checkpoint fixes 2024-03-08 14:35:44 -08:00
bennykok 1bfbd91708 feat(plugin): add models endpoints for listing out all folder paths for debug usecase 2024-03-03 16:17:43 -08:00
bennykok a640e1eb79 fix(plugin): kill pending prompt if new streaming prompts comes in 2024-03-02 12:44:42 -08:00
bennykok 011d36edce fix(plugin): default_value to be optional in streaming image input 2024-03-02 12:22:25 -08:00
bennykok 3df549c25c feat: add ws streaming input 2024-03-02 00:47:28 -08:00
bennykok 619a9728c0 fix(plugin): prompt expansion node seed generation error 2024-02-29 19:09:33 -08:00
bennykok 410d03cd2b fix(plugin): output_id is also included in the binary data back 2024-02-29 11:40:36 -08:00
bennykok 32c6d1215b feat(plugin): streaming file type support, webp and jepg, quality settings 2024-02-28 14:28:39 -08:00
bennykok 9e79c434a9 fix(plugin): make sure number input nodes takes down to 0.01 steps and its casted to float 2024-02-28 12:09:18 -08:00
bennykok 19511e55ba fix(plugin): make sure number input nodes takes down to 0.01 steps 2024-02-28 11:59:26 -08:00
bennykok 2d59fd2b1b feat(plugin): update run status for ws request 2024-02-27 19:45:10 -08:00
bennykok 542b72bde5 fix(plugin): deploy login button 2024-02-26 13:09:48 -08:00
bennykok 7b653201ae fix(plugin): update prompt metadata status properly with realtime prompt 2024-02-26 00:09:56 -08:00
bennykok 1c9c32e9e4 fix(plugin): client id wrongly set causing not sending out ws event 2024-02-25 23:52:20 -08:00
bennykok 97096a9035 feat(plugin): send live_status and elapsed_time 2024-02-25 22:48:22 -08:00
bennykok e87bb63c6f fix(plugin): is_realtime check failed causing everything to not upload 2024-02-25 22:48:22 -08:00
bennykok a643fa0999 fix(plugin): remove file upload + status update from is_realtime prompt 2024-02-25 17:25:41 -08:00
bennykok cc31840d41 fix(plugin): comfy_deploy_check_ws_status 2024-02-25 00:18:07 -08:00
bennykok 25e62af24c refactor(plugin): add prompt_metadata types and refactor from dict to data model 2024-02-24 23:57:32 -08:00
bennykok 9d0ded7ecc feat(plugin): display workflow name on deploy
- remove 2 seconds delay
- use comfy deploy for dependency viewer
- display user / org label
- when login with comfy deploy, ensure save and re load the current url
2024-02-24 23:57:32 -08:00
bennykok ec620dbc53 feat(plugin): load workflow from ws url params 2024-02-24 13:29:56 -08:00
bennykok 45d37879c2 fix: not returning images in websocket output node 2024-02-23 15:09:31 -08:00
bennykok ddbf6848a7 feat(plugin): add output ws image node 2024-02-23 14:03:12 -08:00
nick 4ce2c98ae9 Merge branch 'license-update-agpl' 2024-02-19 08:52:29 -08:00
bennykok 6e068590a0 chore: bump comfyui-json version 2024-02-19 18:53:50 +08:00
18 changed files with 1975 additions and 455 deletions
+21
View File
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,25 @@
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)"}
+4 -4
View File
@@ -16,7 +16,7 @@ class ComfyUIDeployExternalCheckpoint:
), ),
}, },
"optional": { "optional": {
"default_checkpoint_name": (folder_paths.get_filename_list("checkpoints"), ), "default_value": (folder_paths.get_filename_list("checkpoints"), ),
} }
} }
@@ -27,12 +27,12 @@ class ComfyUIDeployExternalCheckpoint:
CATEGORY = "deploy" CATEGORY = "deploy"
def run(self, input_id, default_checkpoint_name=None): def run(self, input_id, default_value=None):
import requests import requests
import os import os
import uuid import uuid
if input_id and input_id.startswith('http'): if default_value.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["checkpoints"][0][0]) print(folder_paths.folder_names_and_paths["checkpoints"][0][0])
@@ -59,7 +59,7 @@ class ComfyUIDeployExternalCheckpoint:
out_file.write(chunk) out_file.write(chunk)
return (unique_filename,) return (unique_filename,)
else: else:
return (default_checkpoints_name,) return (default_value,)
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
+85
View File
@@ -0,0 +1,85 @@
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)"}
+17 -8
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,20 +32,29 @@ class ComfyUIDeployExternalLora:
import os import os
import uuid import uuid
if input_id and input_id.startswith('http'): if default_lora_name.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(folder_paths.folder_names_and_paths["loras"][0][0], unique_filename) destination_path = os.path.join(
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(input_id, headers={'User-Agent': 'Mozilla/5.0'}, allow_redirects=True) response = requests.get(
with open(destination_path, 'wb') as out_file: input_id,
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 = {"ComfyUIDeployExternalLora": "External Lora (ComfyUI Deploy)"} NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyUIDeployExternalLora": "External Lora (ComfyUI Deploy)"
}
+6 -3
View File
@@ -16,7 +16,7 @@ class ComfyUIDeployExternalNumber:
"optional": { "optional": {
"default_value": ( "default_value": (
"FLOAT", "FLOAT",
{"multiline": True, "display": "number", "default": 0}, {"multiline": True, "display": "number", "default": 0, "step": 0.01},
), ),
} }
} }
@@ -29,9 +29,12 @@ class ComfyUIDeployExternalNumber:
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 not input_id.strip().isdigit(): try:
float_value = float(input_id)
print("my number", float_value)
return [float_value]
except ValueError:
return [default_value] return [default_value]
return [int(input_id)]
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalNumber": ComfyUIDeployExternalNumber} NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalNumber": ComfyUIDeployExternalNumber}
+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 not input_id.strip().isdigit(): if not input_id or (isinstance(input_id, str) and not input_id.strip().isdigit()):
return [default_value] return [default_value]
return [int(input_id)] return [int(input_id)]
+48
View File
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,78 @@
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
@@ -0,0 +1,594 @@
# 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)"
}
+66
View File
@@ -0,0 +1,66 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
from server import PromptServer, BinaryEventTypes
import asyncio
from globals import streaming_prompt_metadata, max_output_id_length
class ComfyDeployWebscoketImageInput:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_id"},
),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
"optional": {
"default_value": ("IMAGE", ),
"client_id": (
"STRING",
{"multiline": False, "default": ""},
),
}
}
OUTPUT_NODE = True
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("images",)
FUNCTION = "run"
@classmethod
def VALIDATE_INPUTS(s, input_id):
try:
if len(input_id.encode('ascii')) > max_output_id_length:
raise ValueError(f"input_id size is greater than {max_output_id_length} bytes")
except UnicodeEncodeError:
raise ValueError("input_id is not ASCII encodable")
return True
def run(self, input_id, seed, default_value=None ,client_id=None):
# print(streaming_prompt_metadata[client_id].inputs)
if client_id in streaming_prompt_metadata and input_id in streaming_prompt_metadata[client_id].inputs:
if isinstance(streaming_prompt_metadata[client_id].inputs[input_id], Image.Image):
print("Returning image from websocket input")
image = streaming_prompt_metadata[client_id].inputs[input_id]
image = ImageOps.exif_transpose(image)
image = image.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
return [image]
print("Returning default value")
return [default_value]
NODE_CLASS_MAPPINGS = {"ComfyDeployWebscoketImageInput": ComfyDeployWebscoketImageInput}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyDeployWebscoketImageInput": "Image Websocket Input (ComfyDeploy)"}
+71
View File
@@ -0,0 +1,71 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
from server import PromptServer, BinaryEventTypes
import asyncio
from globals import send_image, max_output_id_length
class ComfyDeployWebscoketImageOutput:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"output_id": (
"STRING",
{"multiline": False, "default": "output_id"},
),
"images": ("IMAGE", ),
"file_type": (["WEBP", "PNG", "JPEG"], ),
"quality": ("INT", {"default": 80, "min": 1, "max": 100, "step": 1}),
},
"optional": {
"client_id": (
"STRING",
{"multiline": False, "default": ""},
),
}
# "hidden": {"client_id": "CLIENT_ID"},
}
OUTPUT_NODE = True
RETURN_TYPES = ()
RETURN_NAMES = ("text",)
FUNCTION = "run"
CATEGORY = "output"
@classmethod
def VALIDATE_INPUTS(s, output_id):
try:
if len(output_id.encode('ascii')) > max_output_id_length:
raise ValueError(f"output_id size is greater than {max_output_id_length} bytes")
except UnicodeEncodeError:
raise ValueError("output_id is not ASCII encodable")
return True
def run(self, output_id, images, file_type, quality, client_id):
prompt_server = PromptServer.instance
loop = prompt_server.loop
def schedule_coroutine_blocking(target, *args):
future = asyncio.run_coroutine_threadsafe(target(*args), loop)
return future.result() # This makes the call blocking
for tensor in images:
array = 255.0 * tensor.cpu().numpy()
image = Image.fromarray(np.clip(array, 0, 255).astype(np.uint8))
schedule_coroutine_blocking(send_image, [file_type, image, None, quality], client_id, output_id)
print("Image sent")
return {"ui": {}}
NODE_CLASS_MAPPINGS = {"ComfyDeployWebscoketImageOutput": ComfyDeployWebscoketImageOutput}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyDeployWebscoketImageOutput": "Image Websocket Output (ComfyDeploy)"}
+451 -164
View File
@@ -1,37 +1,51 @@
from io import BytesIO
from aiohttp import web from aiohttp import web
import os import os
import requests import requests
import folder_paths import folder_paths
import json import json
import numpy as np
import server import server
import re
import base64
from PIL import Image from PIL import Image
import io
import time import time
import execution import execution
import random import random
import traceback import traceback
import uuid import uuid
import asyncio import asyncio
import atexit
import logging import logging
import sys
from logging.handlers import RotatingFileHandler
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
import concurrent.futures from typing import List, Union, Any, Optional
from PIL import Image
import copy
import struct
from globals import StreamingPrompt, Status, sockets, SimplePrompt, streaming_prompt_metadata, prompt_metadata
api = None api = None
api_task = None api_task = None
prompt_metadata = {}
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'
print("CD_BYPASS_UPLOAD", bypass_upload)
def clear_current_prompt(sid):
prompt_server = server.PromptServer.instance
to_delete = list(streaming_prompt_metadata[sid].running_prompt_ids) # Convert set to list
print("clearning out prompt: ", to_delete)
for id_to_delete in to_delete:
delete_func = lambda a: a[1] == id_to_delete
prompt_server.prompt_queue.delete_queue_item(delete_func)
print("deleted prompt: ", id_to_delete, prompt_server.prompt_queue.get_tasks_remaining())
streaming_prompt_metadata[sid].running_prompt_ids.clear()
def post_prompt(json_data): def post_prompt(json_data):
prompt_server = server.PromptServer.instance prompt_server = server.PromptServer.instance
@@ -80,19 +94,114 @@ def randomSeed(num_digits=15):
range_end = (10**num_digits) - 1 range_end = (10**num_digits) - 1
return random.randint(range_start, range_end) return random.randint(range_start, range_end)
def apply_random_seed_to_workflow(workflow_api):
"""
Applies a random seed to each element in the workflow_api that has a 'seed' input.
Args:
workflow_api (dict): The workflow API dictionary to modify.
"""
for key in workflow_api:
if 'inputs' in workflow_api[key] and 'seed' in workflow_api[key]['inputs']:
if isinstance(workflow_api[key]['inputs']['seed'], list):
continue
if workflow_api[key]['class_type'] == "PromptExpansion":
workflow_api[key]['inputs']['seed'] = randomSeed(8);
continue
workflow_api[key]['inputs']['seed'] = randomSeed();
def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
# Loop through each of the inputs and replace them
for key, value in workflow_api.items():
if 'inputs' in value:
# Support websocket
if sid is not None:
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
if isinstance(new_value, Image.Image):
continue
# Backward compactibility
value['inputs']["input_id"] = new_value
# Fix for external text default value
if (value["class_type"] == "ComfyUIDeployExternalText"):
value['inputs']["default_value"] = new_value
if (value["class_type"] == "ComfyUIDeployExternalCheckpoint"):
value['inputs']["default_value"] = new_value
if (value["class_type"] == "ComfyUIDeployExternalImageBatch"):
value['inputs']["images"] = new_value
if value["class_type"] == "ComfyUIDeployExternalLora":
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)
print("getting inputs" , inputs.inputs)
apply_inputs_to_workflow(workflow_api, inputs.inputs, sid=sid)
print(workflow_api)
prompt_id = str(uuid.uuid4())
prompt = {
"prompt": workflow_api,
"client_id": sid, #"comfy_deploy_instance", #api.client_id
"prompt_id": prompt_id
}
try:
res = post_prompt(prompt)
inputs.running_prompt_ids.add(prompt_id)
prompt_metadata[prompt_id] = SimplePrompt(
status_endpoint=inputs.status_endpoint,
file_upload_endpoint=inputs.file_upload_endpoint,
workflow_api=workflow_api,
is_realtime=True
)
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()
print(f"error: {error_type}, {e}")
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 prompt_server = server.PromptServer.instance
data = await request.json() data = await request.json()
workflow_api = data.get("workflow_api") # 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 # 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")
for key in workflow_api: # Now it handles directly in here
if 'inputs' in workflow_api[key] and 'seed' in workflow_api[key]['inputs']: apply_random_seed_to_workflow(workflow_api)
workflow_api[key]['inputs']['seed'] = randomSeed() apply_inputs_to_workflow(workflow_api, inputs)
prompt = { prompt = {
"prompt": workflow_api, "prompt": workflow_api,
@@ -100,11 +209,11 @@ async def comfy_deploy_run(request):
"prompt_id": prompt_id "prompt_id": prompt_id
} }
prompt_metadata[prompt_id] = { prompt_metadata[prompt_id] = SimplePrompt(
'status_endpoint': data.get('status_endpoint'), status_endpoint=data.get('status_endpoint'),
'file_upload_endpoint': data.get('file_upload_endpoint'), file_upload_endpoint=data.get('file_upload_endpoint'),
'workflow_api': workflow_api workflow_api=workflow_api
} )
try: try:
res = post_prompt(prompt) res = post_prompt(prompt)
@@ -148,7 +257,6 @@ async def comfy_deploy_run(request):
return web.json_response(res, status=status) return web.json_response(res, status=status)
sockets = dict()
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("\\")
@@ -179,63 +287,22 @@ async def compute_sha256_checksum(filepath):
sha256.update(chunk) sha256.update(chunk)
return sha256.hexdigest() return sha256.hexdigest()
# def hash_chunk(start_end, filepath): @server.PromptServer.instance.routes.get('/comfyui-deploy/models')
# """Hash a specific chunk of the file.""" async def get_installed_models(request):
# start, end = start_end # Directly return the list of paths as JSON
# sha256 = hashlib.sha256() new_dict = {}
# with open(filepath, 'rb') as f: for key, value in folder_paths.folder_names_and_paths.items():
# f.seek(start) # Convert set to list for JSON compatibility
# chunk = f.read(end - start) # for path in value[0]:
# sha256.update(chunk) file_list = folder_paths.get_filename_list(key)
# return sha256.digest() # Return the digest of the chunk value_json_compatible = (value[0], list(value[1]), file_list)
new_dict[key] = value_json_compatible
# async def compute_sha256_checksum(filepath): # print(new_dict)
# file_size = os.path.getsize(filepath) return web.json_response(new_dict)
# parts = 1 # Or any other division based on file size or desired concurrency
# part_size = file_size // parts
# start_end_ranges = [(i * part_size, min((i + 1) * part_size, file_size)) for i in range(parts)]
# print(start_end_ranges, file_size)
# loop = asyncio.get_running_loop()
# # Use ThreadPoolExecutor to process chunks in parallel
# with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
# futures = [loop.run_in_executor(executor, hash_chunk, start_end, filepath) for start_end in start_end_ranges]
# chunk_hashes = await asyncio.gather(*futures)
# # Combine the hashes sequentially
# final_sha256 = hashlib.sha256()
# for chunk_hash in chunk_hashes:
# final_sha256.update(chunk_hash)
# return final_sha256.hexdigest()
# def hash_chunk(filepath):
# chunk_size = 1024 * 256 # 256KB per chunk
# sha256 = hashlib.sha256()
# with open(filepath, 'rb') as f:
# while True:
# chunk = f.read(chunk_size)
# if not chunk:
# break # End of file
# sha256.update(chunk)
# return sha256.hexdigest()
# async def compute_sha256_checksum(filepath):
# print("computing sha256 checksum")
# filepath = get_comfyui_path_from_file_path(filepath)
# loop = asyncio.get_running_loop()
# with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# task = loop.run_in_executor(executor, hash_chunk, filepath)
# return await task
# This is start uploading the files to Comfy Deploy # This is start uploading the files to Comfy Deploy
@server.PromptServer.instance.routes.post('/comfyui-deploy/upload-file') @server.PromptServer.instance.routes.post('/comfyui-deploy/upload-file')
async def upload_file(request): async def upload_file_endpoint(request):
data = await request.json() data = await request.json()
file_path = data.get("file_path") file_path = data.get("file_path")
@@ -290,7 +357,7 @@ async def upload_file(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:
@@ -317,26 +384,58 @@ async def upload_file(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 file_path is None: if not file_path:
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
file_path = os.path.join(base, file_path) full_file_path = os.path.join(base, file_path)
# print("file_path", file_path)
start_time = time.time() # Capture the start time # Check if the file hash is in the cache
file_hash = await compute_sha256_checksum( if full_file_path in file_hash_cache:
file_path file_hash = file_hash_cache[full_file_path]
) else:
end_time = time.time() # Capture the end time after the code execution start_time = time.time()
elapsed_time = end_time - start_time # Calculate the elapsed time file_hash = await compute_sha256_checksum(full_file_path)
print(f"Execution time: {elapsed_time} seconds") end_time = time.time()
elapsed_time = end_time - start_time
print(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
}) })
@@ -344,6 +443,16 @@ async def get_file_hash(request):
return web.json_response({ return web.json_response({
"error": str(e) "error": str(e)
}, status=500) }, status=500)
async def update_realtime_run_status(realtime_id: str, status_endpoint: str, status: Status):
body = {
"run_id": realtime_id,
"status": status.value,
}
# requests.post(status_endpoint, json=body)
async with aiohttp.ClientSession() as session:
async with session.post(status_endpoint, json=body) as response:
pass
@server.PromptServer.instance.routes.get('/comfyui-deploy/ws') @server.PromptServer.instance.routes.get('/comfyui-deploy/ws')
async def websocket_handler(request): async def websocket_handler(request):
@@ -357,36 +466,135 @@ async def websocket_handler(request):
sid = uuid.uuid4().hex sid = uuid.uuid4().hex
sockets[sid] = ws sockets[sid] = ws
auth_token = request.rel_url.query.get('token', None)
get_workflow_endpoint_url = request.rel_url.query.get('workflow_endpoint', None)
realtime_id = request.rel_url.query.get('realtime_id', None)
status_endpoint = request.rel_url.query.get('status_endpoint', None)
if auth_token is not None and get_workflow_endpoint_url is not None:
async with aiohttp.ClientSession() as session:
headers = {'Authorization': f'Bearer {auth_token}'}
async with session.get(get_workflow_endpoint_url, headers=headers) as response:
if response.status == 200:
workflow = await response.json()
print("Loaded workflow version ",workflow["version"])
streaming_prompt_metadata[sid] = StreamingPrompt(
workflow_api=workflow["workflow_api"],
auth_token=auth_token,
inputs={},
status_endpoint=status_endpoint,
file_upload_endpoint=request.rel_url.query.get('file_upload_endpoint', None),
)
await update_realtime_run_status(realtime_id, status_endpoint, Status.RUNNING)
# await send("workflow_api", workflow_api, sid)
else:
error_message = await response.text()
print(f"Failed to fetch workflow endpoint. Status: {response.status}, Error: {error_message}")
# await send("error", {"message": error_message}, sid)
try: try:
# Send initial state to the new client # Send initial state to the new client
await send("status", { 'sid': sid }, sid) await send("status", { 'sid': sid }, sid)
if cd_enable_log: # Make sure when its connected via client, the full log is not being sent
if cd_enable_log and get_workflow_endpoint_url is None:
await send_first_time_log(sid) await send_first_time_log(sid)
async for msg in ws: async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
try:
data = json.loads(msg.data)
print(data)
event_type = data.get('event')
if event_type == 'input':
print("Got input: ", data.get("inputs"))
input = data.get('inputs')
streaming_prompt_metadata[sid].inputs.update(input)
elif event_type == 'queue_prompt':
clear_current_prompt(sid)
send_prompt(sid, streaming_prompt_metadata[sid])
else:
# Handle other event types
pass
except json.JSONDecodeError:
print('Failed to decode JSON from message')
if msg.type == aiohttp.WSMsgType.BINARY:
data = msg.data
event_type, = struct.unpack("<I", data[:4])
if event_type == 0: # Image input
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 = input_id_bytes.decode('ascii').strip() # Decode the input ID from ASCII
print(event_type)
print(image_type_code)
print(input_id)
image_data = data[32:] # The rest is the image data
if image_type_code == 1:
image_type = "JPEG"
elif image_type_code == 2:
image_type = "PNG"
elif image_type_code == 3:
image_type = "WEBP"
else:
print("Unknown image type code:", image_type_code)
return
image = Image.open(BytesIO(image_data))
# Check if the input ID already exists and replace the input with the new one
if input_id in streaming_prompt_metadata[sid].inputs:
# If the input exists, we assume it's an image and attempt to close it to free resources
try:
existing_image = streaming_prompt_metadata[sid].inputs[input_id]
if hasattr(existing_image, 'close'):
existing_image.close()
except Exception as e:
print(f"Error closing previous image for input ID {input_id}: {e}")
streaming_prompt_metadata[sid].inputs[input_id] = image
# clear_current_prompt(sid)
# send_prompt(sid, streaming_prompt_metadata[sid])
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:
print('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)
if realtime_id is not None:
await update_realtime_run_status(realtime_id, status_endpoint, Status.SUCCESS)
return ws return ws
@server.PromptServer.instance.routes.get('/comfyui-deploy/check-status') @server.PromptServer.instance.routes.get('/comfyui-deploy/check-status')
async def comfy_deploy_check_status(request): async def comfy_deploy_check_status(request):
prompt_server = server.PromptServer.instance
prompt_id = request.rel_url.query.get('prompt_id', None) prompt_id = request.rel_url.query.get('prompt_id', None)
if prompt_id in prompt_metadata and 'status' in prompt_metadata[prompt_id]: if prompt_id in prompt_metadata:
return web.json_response({ return web.json_response({
"status": prompt_metadata[prompt_id]['status'].value "status": prompt_metadata[prompt_id].status.value
}) })
else: else:
return web.json_response({ return web.json_response({
"message": "prompt_id not found" "message": "prompt_id not found"
}) })
@server.PromptServer.instance.routes.get('/comfyui-deploy/check-ws-status')
async def comfy_deploy_check_ws_status(request):
client_id = request.rel_url.query.get('client_id', None)
if client_id in streaming_prompt_metadata:
remaining_queue = 0 # Initialize remaining queue count
for prompt_id in streaming_prompt_metadata[client_id].running_prompt_ids:
prompt_status = prompt_metadata[prompt_id].status
if prompt_status not in [Status.FAILED, Status.SUCCESS]:
remaining_queue += 1 # Increment for each prompt still running
return web.json_response({"remaining_queue": remaining_queue})
else:
return web.json_response({"message": "client_id not found"}, status=404)
async def send(event, data, sid=None): async def send(event, data, sid=None):
try: try:
# message = {"event": event, "data": data}
if sid: if sid:
ws = sockets.get(sid) ws = sockets.get(sid)
if ws != None and not ws.closed: # Check if the WebSocket connection is open and not closing if ws != None and not ws.closed: # Check if the WebSocket connection is open and not closing
@@ -402,53 +610,74 @@ async def send(event, data, sid=None):
logging.basicConfig(level=logging.INFO) 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):
# print("INTERNAL:", event, data, sid) # print("INTERNAL:", event, data, sid)
prompt_id = data.get('prompt_id') prompt_id = data.get('prompt_id')
target_sid = sid
if target_sid == "comfy_deploy_instance":
target_sid = None
# now we send everything # now we send everything
await asyncio.wait([ await asyncio.wait([
asyncio.create_task(send(event, data)), asyncio.create_task(send(event, data, sid=target_sid)),
asyncio.create_task(self.send_json_original(event, data, sid)) asyncio.create_task(self.send_json_original(event, data, sid))
]) ])
if event == 'execution_start': if event == 'execution_start':
update_run(prompt_id, Status.RUNNING) update_run(prompt_id, Status.RUNNING)
if prompt_id in prompt_metadata:
prompt_metadata[prompt_id].start_time = time.perf_counter()
# the last executing event is none, then the workflow is finished # the last executing event is none, then the workflow is finished
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):
update_run(prompt_id, Status.SUCCESS) update_run(prompt_id, Status.SUCCESS)
if prompt_id in prompt_metadata:
current_time = time.perf_counter()
if prompt_metadata[prompt_id].start_time is not None:
elapsed_time = current_time - prompt_metadata[prompt_id].start_time
print(f"Elapsed time: {elapsed_time} seconds")
await send("elapsed_time", {
"prompt_id": prompt_id,
"elapsed_time": elapsed_time
}, sid=sid)
if event == 'executing' and data.get('node') is not None: if event == 'executing' and data.get('node') is not None:
node = data.get('node') node = data.get('node')
if prompt_id in prompt_metadata: if prompt_id in prompt_metadata:
if 'progress' not in prompt_metadata[prompt_id]: # if 'progress' not in prompt_metadata[prompt_id]:
prompt_metadata[prompt_id]["progress"] = set() # prompt_metadata[prompt_id]["progress"] = set()
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)
# print("calculated_progress", calculated_progress) # print("calculated_progress", calculated_progress)
if 'last_updated_node' in prompt_metadata[prompt_id] 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']
print("updating run live status", class_type) print("updating run live status", class_type)
await send("live_status", {
"prompt_id": prompt_id,
"current_node": class_type,
"progress": calculated_progress,
}, sid=sid)
await update_run_live_status(prompt_id, "Executing " + class_type, calculated_progress) await update_run_live_status(prompt_id, "Executing " + class_type, calculated_progress)
if event == 'execution_cached' and data.get('nodes') is not None: if event == 'execution_cached' and data.get('nodes') is not None:
if prompt_id in prompt_metadata: if prompt_id in prompt_metadata:
if 'progress' not in prompt_metadata[prompt_id]: # if 'progress' not in prompt_metadata[prompt_id]:
prompt_metadata[prompt_id]["progress"] = set() # prompt_metadata[prompt_id].progress = set()
if 'nodes' in data: if 'nodes' in data:
for node in data.get('nodes', []): for node in data.get('nodes', []):
prompt_metadata[prompt_id]["progress"].add(node) prompt_metadata[prompt_id].progress.add(node)
# prompt_metadata[prompt_id]["progress"].update(data.get('nodes')) # prompt_metadata[prompt_id]["progress"].update(data.get('nodes'))
if event == 'execution_error': if event == 'execution_error':
@@ -458,18 +687,19 @@ async def send_json_override(self, event, data, sid=None):
# 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:
print("executed", data)
if prompt_id in prompt_metadata:
node = data.get('node')
class_type = prompt_metadata[prompt_id].workflow_api[node]['class_type']
print("executed", class_type)
if class_type == "PreviewImage":
print("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'))
class Status(Enum):
NOT_STARTED = "not-started"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
UPLOADING = "uploading"
# Global variable to keep track of the last read line number # Global variable to keep track of the last read line number
last_read_line_number = 0 last_read_line_number = 0
@@ -477,9 +707,12 @@ async def update_run_live_status(prompt_id, live_status, calculated_progress: fl
if prompt_id not in prompt_metadata: if prompt_id not in prompt_metadata:
return return
if prompt_metadata[prompt_id].is_realtime is True:
return
print("progress", calculated_progress) print("progress", calculated_progress)
status_endpoint = prompt_metadata[prompt_id]['status_endpoint'] status_endpoint = prompt_metadata[prompt_id].status_endpoint
body = { body = {
"run_id": prompt_id, "run_id": prompt_id,
"live_status": live_status, "live_status": live_status,
@@ -491,19 +724,25 @@ async def update_run_live_status(prompt_id, live_status, calculated_progress: fl
pass pass
def update_run(prompt_id, 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:
return return
# if prompt_metadata[prompt_id].start_time is None and status == Status.RUNNING:
# if its realtime prompt we need to skip that.
if prompt_metadata[prompt_id].is_realtime is True:
prompt_metadata[prompt_id].status = status
return
if ('status' not in prompt_metadata[prompt_id] or 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 ('status' in prompt_metadata[prompt_id] and prompt_metadata[prompt_id]['status'] == Status.FAILED): if (prompt_metadata[prompt_id].status is Status.FAILED):
return return
status_endpoint = prompt_metadata[prompt_id]['status_endpoint'] status_endpoint = prompt_metadata[prompt_id].status_endpoint
body = { body = {
"run_id": prompt_id, "run_id": prompt_id,
"status": status.value, "status": status.value,
@@ -544,13 +783,12 @@ def update_run(prompt_id, status: Status):
except Exception as log_error: except Exception as log_error:
print(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()
print(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
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"):
@@ -582,7 +820,7 @@ async def upload_file(prompt_id, filename, subfolder=None, content_type="image/p
print("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
filename = quote(filename) filename = quote(filename)
prompt_id = quote(prompt_id) prompt_id = quote(prompt_id)
@@ -590,39 +828,60 @@ 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
print("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:
print("upload file response", response.status) print("Upload file response", response.status)
end_time = time.time() # End timing after the request is complete
print("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 'uploading_nodes' in prompt_metadata[prompt_id] and len(prompt_metadata[prompt_id]['uploading_nodes']) > 0: if prompt_id in prompt_metadata and len(prompt_metadata[prompt_id].uploading_nodes) > 0:
print("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
print("no pending upload") print("no pending upload")
return False return False
def mark_prompt_done(prompt_id): def mark_prompt_done(prompt_id):
"""
Mark the prompt as done in the prompt metadata.
Args:
prompt_id (str): The ID of the prompt to mark as done.
"""
if prompt_id in prompt_metadata: if prompt_id in prompt_metadata:
prompt_metadata[prompt_id]["done"] = True prompt_metadata[prompt_id].done = True
print("Prompt done") print("Prompt done")
def is_prompt_done(prompt_id): def is_prompt_done(prompt_id: str):
if prompt_id in prompt_metadata and "done" in prompt_metadata[prompt_id]: """
if prompt_metadata[prompt_id]["done"] == True: Check if the prompt with the given ID is marked as done.
return True
Args:
prompt_id (str): The ID of the prompt to check.
Returns:
bool: True if the prompt is marked as done, False otherwise.
"""
if prompt_id in prompt_metadata and prompt_metadata[prompt_id].done is True:
return True
return False return False
# Use to handle upload error and send back to ComfyDeploy # Use to handle upload error and send back to ComfyDeploy
@@ -644,17 +903,17 @@ async def handle_error(prompt_id, data, e: Exception):
print(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, data, uploading, have_error=False, node_id=None): async def update_file_status(prompt_id: str, data, uploading, have_error=False, node_id=None):
if 'uploading_nodes' not in prompt_metadata[prompt_id]: # if 'uploading_nodes' not in prompt_metadata[prompt_id]:
prompt_metadata[prompt_id]['uploading_nodes'] = set() # prompt_metadata[prompt_id]['uploading_nodes'] = set()
if node_id is not None: if node_id is not None:
if uploading: if uploading:
prompt_metadata[prompt_id]['uploading_nodes'].add(node_id) prompt_metadata[prompt_id].uploading_nodes.add(node_id)
else: else:
prompt_metadata[prompt_id]['uploading_nodes'].discard(node_id) prompt_metadata[prompt_id].uploading_nodes.discard(node_id)
print(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:
@@ -666,39 +925,52 @@ async def update_file_status(prompt_id, data, uploading, have_error=False, node_
# 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:
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):
update_run(prompt_id, Status.SUCCESS) update_run(prompt_id, Status.SUCCESS)
print("Status: SUCCUSS") # print("Status: SUCCUSS")
await send("success", { await send("success", {
"prompt_id": prompt_id, "prompt_id": prompt_id,
}) })
async def handle_upload(prompt_id, data, key, content_type_key, default_content_type): 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=item.get(content_type_key, default_content_type) content_type=file_type
) )
# Upload files in the background # Upload files in the background
async def upload_in_background(prompt_id, data, node_id=None, have_upload=True): async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=True):
try: try:
await handle_upload(prompt_id, data, 'images', "content_type", "image/png") await handle_upload(prompt_id, data, 'images', "content_type", "image/png")
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)
@@ -706,32 +978,41 @@ async def upload_in_background(prompt_id, data, node_id=None, have_upload=True):
await handle_error(prompt_id, data, e) await handle_error(prompt_id, data, e)
async def update_run_with_output(prompt_id, data, node_id=None): async def update_run_with_output(prompt_id, data, node_id=None):
if prompt_id in prompt_metadata: if prompt_id not in prompt_metadata:
status_endpoint = prompt_metadata[prompt_id]['status_endpoint'] return
if prompt_metadata[prompt_id].is_realtime is True:
return
status_endpoint = prompt_metadata[prompt_id].status_endpoint
body = { body = {
"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 print("\nhave_upload", have_upload_media, node_id)
print("\nhave_upload", have_upload, node_id)
if have_upload: if have_upload_media:
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)) asyncio.create_task(upload_in_background(prompt_id, data, node_id=node_id, have_upload=have_upload_media))
# 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) requests.post(status_endpoint, json=body)
await send('outputs_uploaded', { await send('outputs_uploaded', {
"prompt_id": prompt_id "prompt_id": prompt_id
}) })
prompt_server.send_json_original = prompt_server.send_json prompt_server.send_json_original = prompt_server.send_json
prompt_server.send_json = send_json_override.__get__(prompt_server, server.PromptServer) prompt_server.send_json = send_json_override.__get__(prompt_server, server.PromptServer)
@@ -781,3 +1062,9 @@ 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})
+115
View File
@@ -0,0 +1,115 @@
import struct
from enum import Enum
import aiohttp
from typing import List, Union, Any, Optional
from PIL import Image, ImageOps
from io import BytesIO
from pydantic import BaseModel as PydanticBaseModel
class BaseModel(PydanticBaseModel):
class Config:
arbitrary_types_allowed = True
class Status(Enum):
NOT_STARTED = "not-started"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
UPLOADING = "uploading"
class StreamingPrompt(BaseModel):
workflow_api: Any
auth_token: str
inputs: dict[str, Union[str, bytes, Image.Image]]
running_prompt_ids: set[str] = set()
status_endpoint: str
file_upload_endpoint: str
class SimplePrompt(BaseModel):
status_endpoint: str
file_upload_endpoint: 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()
prompt_metadata: dict[str, SimplePrompt] = {}
streaming_prompt_metadata: dict[str, StreamingPrompt] = {}
class BinaryEventTypes:
PREVIEW_IMAGE = 1
UNENCODED_PREVIEW_IMAGE = 2
max_output_id_length = 24
async def send_image(image_data, sid=None, output_id:str = None):
max_length = max_output_id_length
output_id = output_id[:max_length]
padded_output_id = output_id.ljust(max_length, '\x00')
encoded_output_id = padded_output_id.encode('ascii', 'replace')
image_type = image_data[0]
image = image_data[1]
max_size = image_data[2]
quality = image_data[3]
if max_size is not None:
if hasattr(Image, 'Resampling'):
resampling = Image.Resampling.BILINEAR
else:
resampling = Image.ANTIALIAS
image = ImageOps.contain(image, (max_size, max_size), resampling)
type_num = 1
if image_type == "JPEG":
type_num = 1
elif image_type == "PNG":
type_num = 2
elif image_type == "WEBP":
type_num = 3
bytesIO = BytesIO()
header = struct.pack(">I", type_num)
# 4 bytes for the type
bytesIO.write(header)
# 10 bytes for the output_id
position_before = bytesIO.tell()
bytesIO.write(encoded_output_id)
position_after = bytesIO.tell()
bytes_written = position_after - position_before
print(f"Bytes written: {bytes_written}")
image.save(bytesIO, format=image_type, quality=quality, compress_level=1)
preview_bytes = bytesIO.getvalue()
await send_bytes(BinaryEventTypes.PREVIEW_IMAGE, preview_bytes, sid=sid)
async def send_socket_catch_exception(function, message):
try:
await function(message)
except (aiohttp.ClientError, aiohttp.ClientPayloadError, ConnectionResetError) as err:
print("send error:", err)
def encode_bytes(event, data):
if not isinstance(event, int):
raise RuntimeError(f"Binary event types must be integers, got {event}")
packed = struct.pack(">I", event)
message = bytearray(packed)
message.extend(data)
return message
async def send_bytes(event, data, sid=None):
message = encode_bytes(event, data)
print("sending image to ", event, sid)
if sid is None:
_sockets = list(sockets.values())
for ws in _sockets:
await send_socket_catch_exception(ws.send_bytes, message)
elif sid in sockets:
await send_socket_catch_exception(sockets[sid].send_bytes, message)
+7 -1
View File
@@ -58,6 +58,9 @@ 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__))
@@ -66,4 +69,7 @@ try:
current_git_commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip() current_git_commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip()
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
@@ -0,0 +1,15 @@
[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 = ""
+4 -1
View File
@@ -1 +1,4 @@
aiofiles aiofiles
pydantic
opencv-python
imageio-ffmpeg
+367 -273
View File
@@ -1,10 +1,18 @@
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].19"; import { generateDependencyGraph } from "https://esm.sh/[email protected].25";
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), "*");
}
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ /** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
/** @type {ComfyExtension} */ /** @type {ComfyExtension} */
const ext = { const ext = {
@@ -18,6 +26,35 @@ 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;
@@ -59,8 +96,8 @@ const ext = {
return; return;
} }
// Adding a delay to wait for the intial graph to load // // Adding a delay to wait for the intial graph to load
await new Promise((resolve) => setTimeout(resolve, 2000)); // await new Promise((resolve) => setTimeout(resolve, 2000));
workflow?.nodes.forEach((x) => { workflow?.nodes.forEach((x) => {
if (x?.type === "ComfyDeploy") { if (x?.type === "ComfyDeploy") {
@@ -152,9 +189,32 @@ const ext = {
async setup() { async setup() {
// const graphCanvas = document.getElementById("graph-canvas"); // const graphCanvas = document.getElementById("graph-canvas");
window.addEventListener("message", (event) => { window.addEventListener("message", async (event) => {
if (!event.data.flow || Object.entries(event.data.flow).length <= 0) try {
return; const message = JSON.parse(event.data);
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);
}
} 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);
}); });
@@ -167,6 +227,18 @@ 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");
}, },
}; };
@@ -267,294 +339,296 @@ function createDynamicUIHtml(data) {
return html; return html;
} }
function addButton() { async function deployWorkflow() {
const menu = document.querySelector(".comfy-menu"); const deploy = document.getElementById("deploy-button");
const deploy = document.createElement("button"); /** @type {LGraph} */
deploy.style.position = "relative"; const graph = app.graph;
deploy.style.display = "block";
deploy.innerHTML = "<div id='button-title'>Deploy</div>";
deploy.onclick = async () => {
/** @type {LGraph} */
const graph = app.graph;
let { endpoint, apiKey, displayName } = getData(); let { endpoint, apiKey, displayName } = getData();
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") { if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
configDialog.show(); configDialog.show();
return; return;
} }
const ok = await confirmDialog.confirm( let deployMeta = graph.findNodesByType("ComfyDeploy");
`Confirm deployment`,
`
<div>
A new version will be deployed, do you confirm? if (deployMeta.length == 0) {
<br><br> const text = await inputDialog.input(
"Create your deployment",
<button style="font-size: 18px;">${displayName}</button> "Workflow name",
<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; 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 includeDeps = document.getElementById("include-deps").checked; const deployMetaNode = deployMeta[0];
const reuseHash = document.getElementById("reuse-hash").checked;
if (endpoint.endsWith("/")) { const workflow_name = deployMetaNode.widgets[0].value;
endpoint = endpoint.slice(0, -1); const workflow_id = deployMetaNode.widgets[1].value;
}
loadingDialog.showLoading("Generating snapshot");
const snapshot = await fetch("/snapshot/get_current").then((x) => x.json()); const ok = await confirmDialog.confirm(
// console.log(snapshot); `Confirm deployment`,
loadingDialog.close(); `
<div>
if (!snapshot) { A new version of <button style="font-size: 18px;">${workflow_name}</button> will be deployed, do you confirm?
showError( <br><br>
"Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager",
);
return;
}
const title = deploy.querySelector("#button-title"); <button style="font-size: 18px;">${displayName}</button>
<br>
<button style="font-size: 18px;">${endpoint}</button>
let deployMeta = graph.findNodesByType("ComfyDeploy"); <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;
if (deployMeta.length == 0) { const includeDeps = document.getElementById("include-deps").checked;
const text = await inputDialog.input( const reuseHash = document.getElementById("reuse-hash").checked;
"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]; if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
loadingDialog.showLoading("Generating snapshot");
const workflow_name = deployMetaNode.widgets[0].value; const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
const workflow_id = deployMetaNode.widgets[1].value; // console.log(snapshot);
loadingDialog.close();
const prompt = await app.graphToPrompt(); if (!snapshot) {
let deps = undefined; showError(
"Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager",
);
return;
}
if (includeDeps) { const title = deploy.querySelector("#button-title");
loadingDialog.showLoading("Fetching existing version");
const existing_workflow = await fetch( const prompt = await app.graphToPrompt();
endpoint + "/api/workflow/" + workflow_id, let deps = undefined;
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
},
)
.then((x) => x.json())
.catch(() => {
return {};
});
loadingDialog.close(); if (includeDeps) {
loadingDialog.showLoading("Fetching existing version");
loadingDialog.showLoading("Generating dependency graph"); const existing_workflow = await fetch(
deps = await generateDependencyGraph({ endpoint + "/api/workflow/" + workflow_id,
workflow_api: prompt.output, {
snapshot: snapshot, method: "GET",
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="${endpoint}/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: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + apiKey, Authorization: "Bearer " + apiKey,
}, },
},
)
.then((x) => x.json())
.catch(() => {
return {};
}); });
console.log(data); loadingDialog.close();
if (data.status !== 200) { loadingDialog.showLoading("Generating dependency graph");
throw new Error(await data.text()); deps = await generateDependencyGraph({
} else { workflow_api: prompt.output,
data = await data.json(); snapshot: snapshot,
} computeFileHash: async (file) => {
console.log(existing_workflow?.dependencies?.models);
loadingDialog.close(); // 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);
title.textContent = "Done"; const match = previousModelHash.find((x) => {
title.style.color = "green"; 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,
});
deployMetaNode.widgets[1].value = data.workflow_id; // Need to find a way to include this if this is not included in comfyui-json level
deployMetaNode.widgets[2].value = data.version; if (
graph.change(); !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",
};
infoDialog.show( loadingDialog.close();
`<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(() => { const depsOk = await confirmDialog.confirm(
title.textContent = "Deploy"; "Check dependencies",
title.style.color = "white"; // JSON.stringify(deps, null, 2),
}, 1000); `
} catch (e) { <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
loadingDialog.close(); <iframe
app.ui.dialog.show(e); style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
console.error(e); src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
title.textContent = "Error"; JSON.stringify(deps),
title.style.color = "red"; )}" />`,
setTimeout(() => { // createDynamicUIHtml(deps),
title.textContent = "Deploy"; );
title.style.color = "white"; if (!depsOk) return;
}, 1000);
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() {
const menu = document.querySelector(".comfy-menu");
const deploy = document.createElement("button");
deploy.id = "deploy-button";
deploy.style.position = "relative";
deploy.style.display = "block";
deploy.innerHTML = "<div id='button-title'>Deploy</div>";
deploy.onclick = async () => {
await deployWorkflow();
}; };
const config = document.createElement("img"); const config = document.createElement("img");
@@ -880,7 +954,10 @@ export class ConfigDialog extends ComfyDialog {
justifyContent: "flex-end", justifyContent: "flex-end",
width: "100%", width: "100%",
}, },
onclick: () => this.save(), onclick: () => {
this.save();
this.close();
},
}, },
[ [
$el("button", { $el("button", {
@@ -891,7 +968,10 @@ export class ConfigDialog extends ComfyDialog {
$el("button", { $el("button", {
type: "button", type: "button",
textContent: "Save", textContent: "Save",
onclick: () => this.save(), onclick: () => {
this.save();
this.close();
},
}), }),
], ],
), ),
@@ -905,20 +985,26 @@ export class ConfigDialog extends ComfyDialog {
} }
save(api_key, displayName) { save(api_key, displayName) {
if (!displayName) displayName = getData().displayName;
const deployOption = this.container.querySelector("#deployOption").value; const deployOption = this.container.querySelector("#deployOption").value;
localStorage.setItem("comfy_deploy_env", deployOption); localStorage.setItem("comfy_deploy_env", deployOption);
const endpoint = this.container.querySelector("#endpoint").value; const endpoint = this.container.querySelector("#endpoint").value;
const apiKey = api_key ?? this.container.querySelector("#apiKey").value; const apiKey = api_key ?? this.container.querySelector("#apiKey").value;
if (!displayName) {
if (apiKey != getData().apiKey) {
displayName = "Custom";
} else {
displayName = getData().displayName;
}
}
saveData({ saveData({
endpoint, endpoint,
apiKey, apiKey,
displayName, displayName,
environment: deployOption, environment: deployOption,
}); });
this.close();
} }
show() { show() {
@@ -941,8 +1027,10 @@ export class ConfigDialog extends ComfyDialog {
data.endpoint data.endpoint
}"> }">
</label> </label>
<label style="color: white;"> <div style="color: white;">
API Key: ${data.displayName ?? ""} API Key: User / Org <button style="font-size: 18px;">${
data.displayName ?? ""
}</button>
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${ <input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${
data.apiKey data.apiKey
}"> }">
@@ -951,12 +1039,15 @@ export class ConfigDialog extends ComfyDialog {
data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy" data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
} }
</button> </button>
</label> </div>
</div> </div>
`; `;
const button = this.container.querySelector("#loginButton"); const button = this.container.querySelector("#loginButton");
button.onclick = () => { button.onclick = () => {
this.save();
const data = getData();
const uuid = const uuid =
Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15); Math.random().toString(36).substring(2, 15);
@@ -973,17 +1064,20 @@ export class ConfigDialog extends ComfyDialog {
this.poll = setInterval(() => { this.poll = setInterval(() => {
fetch(data.endpoint + "/api/auth-response/" + uuid) fetch(data.endpoint + "/api/auth-response/" + uuid)
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then(async (json) => {
if (json.api_key) { if (json.api_key) {
this.save(json.api_key, json.name); this.save(json.api_key, json.name);
this.close();
this.container.querySelector("#apiKey").value = json.api_key; this.container.querySelector("#apiKey").value = json.api_key;
infoDialog.show(); // infoDialog.show();
clearInterval(this.poll); clearInterval(this.poll);
clearTimeout(this.timeout); clearTimeout(this.timeout);
infoDialog.showMessage( // Refresh dialog
const a = await confirmDialog.confirm(
"Authenticated", "Authenticated",
"You will be able to upload workflow to " + json.name, `<div>You will be able to upload workflow to <button style="font-size: 18px; width: fit;">${json.name}</button></div>`,
); );
configDialog.show();
} }
}) })
.catch((error) => { .catch((error) => {