Compare commits

...
74 Commits
Author SHA1 Message Date
BennyKok 8c8f2abc16 Merge branch 'main' into dev 2024-07-07 22:06:54 -07:00
Emmanuel Morales 716790e344 fix(media upload): skip when using the CD_BYPASS_UPLOAD env var (#51)
* fix(image upload): skip when using the CD_BYPASS_UPLOAD env var

* Revert "fix(image upload): skip when using the CD_BYPASS_UPLOAD env var"

This reverts commit 384eda63e6.

* 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 22:04:00 -07:00
nick c4d1b09a24 custom route 2024-06-19 16:52:17 -07:00
nick c6fe88bf66 new route 2024-06-15 17:29:51 -07:00
bennykok c70e08a706 chore(plugin): add log 2024-06-11 17:43:03 -07:00
bennykok daf1669e70 fix: node_error proxy 2024-06-11 17:43:02 -07:00
bennykok 62df715655 fix: prompt error 2024-06-11 17:43:02 -07:00
bennykok 04fd08d5ba fix: streaming event format 2024-06-11 17:43:02 -07:00
bennykok 4a8ef7c77c fix(plugin): event 2024-06-11 17:43:02 -07:00
bennykok 5b8dac37fb feat(plugin): add dispatchAPIEventData 2024-06-11 17:43:02 -07:00
bennykok 875f7f24d1 fix: run issues 2024-06-11 17:43:02 -07:00
bennykok af0fac7afc feat: add streaming endpoint 2024-06-11 17:43:02 -07:00
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 2300 additions and 518 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": {
"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"
def run(self, input_id, default_checkpoint_name=None):
def run(self, input_id, default_value=None):
import requests
import os
import uuid
if input_id and input_id.startswith('http'):
if default_value.startswith('http'):
unique_filename = str(uuid.uuid4()) + ".safetensors"
print(unique_filename)
print(folder_paths.folder_names_and_paths["checkpoints"][0][0])
@@ -59,7 +59,7 @@ class ComfyUIDeployExternalCheckpoint:
out_file.write(chunk)
return (unique_filename,)
else:
return (default_checkpoints_name,)
return (default_value,)
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)"}
+15 -6
View File
@@ -17,7 +17,7 @@ class ComfyUIDeployExternalLora:
},
"optional": {
"default_lora_name": (folder_paths.get_filename_list("loras"),),
}
},
}
RETURN_TYPES = (folder_paths.get_filename_list("loras"),)
@@ -32,20 +32,29 @@ class ComfyUIDeployExternalLora:
import os
import uuid
if input_id and input_id.startswith('http'):
if default_lora_name.startswith("http"):
unique_filename = str(uuid.uuid4()) + ".safetensors"
print(unique_filename)
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("Downloading external lora - " + input_id + " to " + destination_path)
response = requests.get(input_id, headers={'User-Agent': 'Mozilla/5.0'}, allow_redirects=True)
with open(destination_path, 'wb') as out_file:
response = requests.get(
input_id,
headers={"User-Agent": "Mozilla/5.0"},
allow_redirects=True,
)
with open(destination_path, "wb") as out_file:
out_file.write(response.content)
return (unique_filename,)
else:
print(f"using lora: {default_lora_name}")
return (default_lora_name,)
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": {
"default_value": (
"FLOAT",
{"multiline": True, "display": "number", "default": 0},
{"multiline": True, "display": "number", "default": 0, "step": 0.01},
),
}
}
@@ -29,9 +29,12 @@ class ComfyUIDeployExternalNumber:
CATEGORY = "number"
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 [int(input_id)]
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalNumber": ComfyUIDeployExternalNumber}
+1 -1
View File
@@ -29,7 +29,7 @@ class ComfyUIDeployExternalNumberInt:
CATEGORY = "number"
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 [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)"}
+670 -199
View File
File diff suppressed because it is too large Load Diff
+116
View File
@@ -0,0 +1,116 @@
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: Optional[str]
file_upload_endpoint: Optional[str]
class SimplePrompt(BaseModel):
status_endpoint: Optional[str]
file_upload_endpoint: Optional[str]
workflow_api: dict
status: Status = Status.NOT_STARTED
progress: set = set()
last_updated_node: Optional[str] = None,
uploading_nodes: set = set()
done: bool = False
is_realtime: bool = False,
start_time: Optional[float] = None,
sockets = dict()
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)
+6
View File
@@ -58,6 +58,9 @@ if cd_enable_log:
print("** Comfy Deploy logging enabled")
setup()
# Store the original working directory
original_cwd = os.getcwd()
try:
# Get the absolute path of the script's directory
script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -67,3 +70,6 @@ try:
print(f"** Comfy Deploy Revision: {current_git_commit}")
except Exception as 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
View File
@@ -1 +1,5 @@
aiofiles
pydantic
opencv-python
imageio-ffmpeg
logfire
+230 -60
View File
@@ -1,10 +1,90 @@
import { app } from "./app.js";
import { api } from "./api.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>`;
function sendEventToCD(event, data) {
const message = {
type: event,
data: data,
};
window.parent.postMessage(JSON.stringify(message), "*");
}
function dispatchAPIEventData(data) {
const msg = JSON.parse(data);
// Custom parse error
if (msg.error) {
let message = msg.error.message;
if (msg.error.details)
message += ": " + msg.error.details;
for (const [nodeID, nodeError] of Object.entries(
msg.node_errors,
)) {
message += "\n" + nodeError.class_type + ":";
for (const errorReason of nodeError.errors) {
message +=
"\n - " + errorReason.message + ": " + errorReason.details;
}
}
app.ui.dialog.show(message);
if (msg.node_errors) {
app.lastNodeErrors = msg.node_errors;
app.canvas.draw(true, true);
}
}
switch (msg.event) {
case "error":
break;
case "status":
if (msg.data.sid) {
// this.clientId = msg.data.sid;
// window.name = this.clientId; // use window name so it isnt reused when duplicating tabs
// sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow
}
api.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
break;
case "progress":
api.dispatchEvent(new CustomEvent("progress", { detail: msg.data }));
break;
case "executing":
api.dispatchEvent(
new CustomEvent("executing", { detail: msg.data.node }),
);
break;
case "executed":
api.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
break;
case "execution_start":
api.dispatchEvent(
new CustomEvent("execution_start", { detail: msg.data }),
);
break;
case "execution_error":
api.dispatchEvent(
new CustomEvent("execution_error", { detail: msg.data }),
);
break;
case "execution_cached":
api.dispatchEvent(
new CustomEvent("execution_cached", { detail: msg.data }),
);
break;
default:
api.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
// default:
// if (this.#registered.has(msg.type)) {
// } else {
// throw new Error(`Unknown message type ${msg.type}`);
// }
}
}
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
/** @type {ComfyExtension} */
const ext = {
@@ -18,6 +98,34 @@ const ext = {
const auth_token = queryParams.get("auth_token");
const org_display = queryParams.get("org_display");
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();
let endpoint = data.endpoint;
@@ -59,8 +167,8 @@ const ext = {
return;
}
// Adding a delay to wait for the intial graph to load
await new Promise((resolve) => setTimeout(resolve, 2000));
// // Adding a delay to wait for the intial graph to load
// await new Promise((resolve) => setTimeout(resolve, 2000));
workflow?.nodes.forEach((x) => {
if (x?.type === "ComfyDeploy") {
@@ -152,9 +260,37 @@ const ext = {
async setup() {
// const graphCanvas = document.getElementById("graph-canvas");
window.addEventListener("message", (event) => {
if (!event.data.flow || Object.entries(event.data.flow).length <= 0)
return;
window.addEventListener("message", async (event) => {
try {
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);
} else if (message.type === "event") {
dispatchAPIEventData(message.data);
}
// else if (message.type === "refresh") {
// sendEventToCD("cd_plugin_onRefresh");
// }
} catch (error) {
// console.error("Error processing message:", error);
}
// if (!event.data.flow || Object.entries(event.data.flow).length <= 0)
// return;
// updateBlendshapesPrompts(event.data.flow);
});
@@ -167,6 +303,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,14 +415,9 @@ function createDynamicUIHtml(data) {
return html;
}
function addButton() {
const menu = document.querySelector(".comfy-menu");
async function deployWorkflow() {
const deploy = document.getElementById("deploy-button");
const deploy = document.createElement("button");
deploy.style.position = "relative";
deploy.style.display = "block";
deploy.innerHTML = "<div id='button-title'>Deploy</div>";
deploy.onclick = async () => {
/** @type {LGraph} */
const graph = app.graph;
@@ -285,12 +428,37 @@ function addButton() {
return;
}
let deployMeta = graph.findNodesByType("ComfyDeploy");
if (deployMeta.length == 0) {
const text = await inputDialog.input(
"Create your deployment",
"Workflow name",
);
if (!text) return;
console.log(text);
app.graph.beforeChange();
var node = LiteGraph.createNode("ComfyDeploy");
node.configure({
widgets_values: [text],
});
node.pos = [0, 0];
app.graph.add(node);
app.graph.afterChange();
deployMeta = [node];
}
const deployMetaNode = deployMeta[0];
const workflow_name = deployMetaNode.widgets[0].value;
const workflow_id = deployMetaNode.widgets[1].value;
const ok = await confirmDialog.confirm(
`Confirm deployment`,
`
<div>
A new version will be deployed, do you confirm?
A new version of <button style="font-size: 18px;">${workflow_name}</button> will be deployed, do you confirm?
<br><br>
<button style="font-size: 18px;">${displayName}</button>
@@ -332,31 +500,6 @@ function addButton() {
const title = deploy.querySelector("#button-title");
let deployMeta = graph.findNodesByType("ComfyDeploy");
if (deployMeta.length == 0) {
const text = await inputDialog.input(
"Create your deployment",
"Workflow name",
);
if (!text) return;
console.log(text);
app.graph.beforeChange();
var node = LiteGraph.createNode("ComfyDeploy");
node.configure({
widgets_values: [text],
});
node.pos = [0, 0];
app.graph.add(node);
app.graph.afterChange();
deployMeta = [node];
}
const deployMetaNode = deployMeta[0];
const workflow_name = deployMetaNode.widgets[0].value;
const workflow_id = deployMetaNode.widgets[1].value;
const prompt = await app.graphToPrompt();
let deps = undefined;
@@ -412,9 +555,7 @@ function addButton() {
console.log(file);
loadingDialog.showLoading("Generating hash", file);
const hash = await fetch(
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(
file,
)}`,
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(file)}`,
).then((x) => x.json());
loadingDialog.showLoading("Generating hash", file);
console.log(hash);
@@ -424,17 +565,14 @@ function addButton() {
console.log("Uploading ", file);
loadingDialog.showLoading("Uploading file", file);
try {
const { download_url } = await fetch(
`/comfyui-deploy/upload-file`,
{
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();
@@ -474,7 +612,7 @@ function addButton() {
<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(
src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
JSON.stringify(deps),
)}" />`,
// createDynamicUIHtml(deps),
@@ -555,6 +693,18 @@ function addButton() {
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");
@@ -880,7 +1030,10 @@ export class ConfigDialog extends ComfyDialog {
justifyContent: "flex-end",
width: "100%",
},
onclick: () => this.save(),
onclick: () => {
this.save();
this.close();
},
},
[
$el("button", {
@@ -891,7 +1044,10 @@ export class ConfigDialog extends ComfyDialog {
$el("button", {
type: "button",
textContent: "Save",
onclick: () => this.save(),
onclick: () => {
this.save();
this.close();
},
}),
],
),
@@ -905,20 +1061,26 @@ export class ConfigDialog extends ComfyDialog {
}
save(api_key, displayName) {
if (!displayName) displayName = getData().displayName;
const deployOption = this.container.querySelector("#deployOption").value;
localStorage.setItem("comfy_deploy_env", deployOption);
const endpoint = this.container.querySelector("#endpoint").value;
const apiKey = api_key ?? this.container.querySelector("#apiKey").value;
if (!displayName) {
if (apiKey != getData().apiKey) {
displayName = "Custom";
} else {
displayName = getData().displayName;
}
}
saveData({
endpoint,
apiKey,
displayName,
environment: deployOption,
});
this.close();
}
show() {
@@ -941,8 +1103,10 @@ export class ConfigDialog extends ComfyDialog {
data.endpoint
}">
</label>
<label style="color: white;">
API Key: ${data.displayName ?? ""}
<div style="color: white;">
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="${
data.apiKey
}">
@@ -951,12 +1115,15 @@ export class ConfigDialog extends ComfyDialog {
data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
}
</button>
</label>
</div>
</div>
`;
const button = this.container.querySelector("#loginButton");
button.onclick = () => {
this.save();
const data = getData();
const uuid =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
@@ -973,17 +1140,20 @@ export class ConfigDialog extends ComfyDialog {
this.poll = setInterval(() => {
fetch(data.endpoint + "/api/auth-response/" + uuid)
.then((response) => response.json())
.then((json) => {
.then(async (json) => {
if (json.api_key) {
this.save(json.api_key, json.name);
this.close();
this.container.querySelector("#apiKey").value = json.api_key;
infoDialog.show();
// infoDialog.show();
clearInterval(this.poll);
clearTimeout(this.timeout);
infoDialog.showMessage(
// Refresh dialog
const a = await confirmDialog.confirm(
"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) => {