Compare commits

...
Author SHA1 Message Date
nick 177010b0d4 fix: lora 2024-04-24 17:00:16 -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
6 changed files with 542 additions and 340 deletions
+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)"}
+20 -13
View File
@@ -32,19 +32,26 @@ class ComfyUIDeployExternalLora:
import os import os
import uuid import uuid
if input_id and input_id.startswith('http'): print('external lora using')
unique_filename = str(uuid.uuid4()) + ".safetensors" print("input id: ", input_id)
print(unique_filename) print("default lora : ", default_lora_name)
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) if input_id:
print(destination_path) if input_id.startswith('http'):
print("Downloading external lora - " + input_id + " to " + destination_path) unique_filename = str(uuid.uuid4()) + ".safetensors"
response = requests.get(input_id, headers={'User-Agent': 'Mozilla/5.0'}, allow_redirects=True) print(unique_filename)
with open(destination_path, 'wb') as out_file: print(folder_paths.folder_names_and_paths["loras"][0][0])
out_file.write(response.content) destination_path = os.path.join(folder_paths.folder_names_and_paths["loras"][0][0], unique_filename)
return (unique_filename,) print(destination_path)
else: print("Downloading external lora - " + input_id + " to " + destination_path)
return (default_lora_name,) 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:
return (input_id,)
return (default_lora_name,)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalLora": ComfyUIDeployExternalLora} NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalLora": ComfyUIDeployExternalLora}
+86 -54
View File
@@ -13,7 +13,6 @@ import traceback
import uuid import uuid
import asyncio import asyncio
import logging import logging
from enum import Enum
from urllib.parse import quote from urllib.parse import quote
import threading import threading
import hashlib import hashlib
@@ -24,30 +23,11 @@ from PIL import Image
import copy import copy
import struct import struct
from globals import StreamingPrompt, sockets, streaming_prompt_metadata, BaseModel from globals import StreamingPrompt, Status, sockets, SimplePrompt, streaming_prompt_metadata, prompt_metadata
class Status(Enum):
NOT_STARTED = "not-started"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
UPLOADING = "uploading"
class 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,
api = None api = None
api_task = None api_task = None
prompt_metadata: dict[str, SimplePrompt] = {}
cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true' cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true'
cd_enable_run_log = os.environ.get('CD_ENABLE_RUN_LOG', 'false').lower() == 'true' cd_enable_run_log = os.environ.get('CD_ENABLE_RUN_LOG', 'false').lower() == 'true'
@@ -126,30 +106,26 @@ def apply_random_seed_to_workflow(workflow_api):
continue continue
workflow_api[key]['inputs']['seed'] = randomSeed(); workflow_api[key]['inputs']['seed'] = randomSeed();
def send_prompt(sid: str, inputs: StreamingPrompt): def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
# workflow_api = inputs.workflow_api
workflow_api = copy.deepcopy(inputs.workflow_api)
# Random seed
apply_random_seed_to_workflow(workflow_api)
print("getting inputs" , inputs.inputs)
# Loop through each of the inputs and replace them # Loop through each of the inputs and replace them
for key, value in workflow_api.items(): for key, value in workflow_api.items():
if 'inputs' in value: if 'inputs' in value:
if (value["class_type"] == "ComfyDeployWebscoketImageOutput"):
value['inputs']["client_id"] = sid
if (value["class_type"] == "ComfyDeployWebscoketImageInput"):
value['inputs']["client_id"] = sid
if "input_id" in value['inputs'] and value['inputs']['input_id'] in inputs.inputs: # Support websocket
new_value = inputs.inputs[value['inputs']['input_id']] 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 value['inputs']['input_id'] in inputs:
new_value = inputs[value['inputs']['input_id']]
# Lets skip it if its an image # Lets skip it if its an image
if isinstance(new_value, Image.Image): if isinstance(new_value, Image.Image):
continue continue
# Backward compactibility
value['inputs']["input_id"] = new_value value['inputs']["input_id"] = new_value
# Fix for external text default value # Fix for external text default value
@@ -159,6 +135,19 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
if (value["class_type"] == "ComfyUIDeployExternalCheckpoint"): if (value["class_type"] == "ComfyUIDeployExternalCheckpoint"):
value['inputs']["default_value"] = new_value value['inputs']["default_value"] = new_value
if (value["class_type"] == "ComfyUIDeployExternalImageBatch"):
value['inputs']["images"] = new_value
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) print(workflow_api)
@@ -191,15 +180,15 @@ 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")
# Now it handles directly in here
apply_random_seed_to_workflow(workflow_api) apply_random_seed_to_workflow(workflow_api)
# for key in workflow_api: apply_inputs_to_workflow(workflow_api, inputs)
# if 'inputs' in workflow_api[key] and 'seed' in workflow_api[key]['inputs']:
# workflow_api[key]['inputs']['seed'] = randomSeed()
prompt = { prompt = {
"prompt": workflow_api, "prompt": workflow_api,
@@ -382,26 +371,58 @@ async def upload_file_endpoint(request):
}, status=500) }, status=500)
script_dir = os.path.dirname(os.path.abspath(__file__))
# Assuming the cache file is stored in the same directory as this script
CACHE_FILE_PATH = script_dir + '/file-hash-cache.json'
# Global in-memory cache
file_hash_cache = {}
# Load cache from disk at startup
def load_cache():
global file_hash_cache
try:
with open(CACHE_FILE_PATH, 'r') as cache_file:
file_hash_cache = json.load(cache_file)
except (FileNotFoundError, json.JSONDecodeError):
file_hash_cache = {}
# Save cache to disk
def save_cache():
with open(CACHE_FILE_PATH, 'w') as cache_file:
json.dump(file_hash_cache, cache_file)
# Initialize cache on application start
load_cache()
@server.PromptServer.instance.routes.get('/comfyui-deploy/get-file-hash') @server.PromptServer.instance.routes.get('/comfyui-deploy/get-file-hash')
async def get_file_hash(request): async def get_file_hash(request):
file_path = request.rel_url.query.get('file_path', '') file_path = request.rel_url.query.get('file_path', '')
if 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
}) })
@@ -653,6 +674,15 @@ 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'))
@@ -696,7 +726,7 @@ def update_run(prompt_id: str, status: Status):
if (prompt_metadata[prompt_id].status != status): if (prompt_metadata[prompt_id].status != status):
# when the status is already failed, we don't want to update it to success # when the status is already failed, we don't want to update it to success
if ('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
@@ -892,6 +922,9 @@ async def update_file_status(prompt_id: str, data, uploading, have_error=False,
async def handle_upload(prompt_id: str, data, key: str, content_type_key: str, default_content_type: str): async def handle_upload(prompt_id: str, data, key: str, content_type_key: str, default_content_type: str):
items = data.get(key, []) items = data.get(key, [])
for item in items: for item in items:
# # Skipping temp files
if item.get("type") == "temp":
continue
await upload_file( await upload_file(
prompt_id, prompt_id,
item.get("filename"), item.get("filename"),
@@ -900,7 +933,6 @@ async def handle_upload(prompt_id: str, data, key: str, content_type_key: str, d
content_type=item.get(content_type_key, default_content_type) content_type=item.get(content_type_key, default_content_type)
) )
# Upload files in the background # Upload files in the background
async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=True): async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=True):
try: try:
+21 -3
View File
@@ -1,17 +1,22 @@
import struct import struct
from enum import Enum
import aiohttp import aiohttp
from typing import List, Union, Any, Optional from typing import List, Union, Any, Optional
from PIL import Image, ImageOps from PIL import Image, ImageOps
from io import BytesIO from io import BytesIO
from pydantic import BaseModel as PydanticBaseModel from pydantic import BaseModel as PydanticBaseModel
class BaseModel(PydanticBaseModel): class BaseModel(PydanticBaseModel):
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
class Status(Enum):
NOT_STARTED = "not-started"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
UPLOADING = "uploading"
class StreamingPrompt(BaseModel): class StreamingPrompt(BaseModel):
workflow_api: Any workflow_api: Any
auth_token: str auth_token: str
@@ -20,7 +25,20 @@ class StreamingPrompt(BaseModel):
status_endpoint: str status_endpoint: str
file_upload_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() sockets = dict()
prompt_metadata: dict[str, SimplePrompt] = {}
streaming_prompt_metadata: dict[str, StreamingPrompt] = {} streaming_prompt_metadata: dict[str, StreamingPrompt] = {}
class BinaryEventTypes: class BinaryEventTypes:
+6
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__))
@@ -67,3 +70,6 @@ try:
print(f"** Comfy Deploy Revision: {current_git_commit}") print(f"** Comfy Deploy Revision: {current_git_commit}")
except Exception as e: except Exception as e:
print(f"** Comfy Deploy failed to get current git commit: {str(e)}") print(f"** Comfy Deploy failed to get current git commit: {str(e)}")
finally:
# Change back to the original directory
os.chdir(original_cwd)
+313 -259
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]2"; import { generateDependencyGraph } from "https://esm.sh/[email protected]5";
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,11 @@ 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";
}
const data = getData(); const data = getData();
let endpoint = data.endpoint; let endpoint = data.endpoint;
@@ -152,9 +165,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 +203,17 @@ 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 +314,301 @@ 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;
} }
let deployMeta = graph.findNodesByType("ComfyDeploy"); let deployMeta = graph.findNodesByType("ComfyDeploy");
if (deployMeta.length == 0) { if (deployMeta.length == 0) {
const text = await inputDialog.input( const text = await inputDialog.input(
"Create your deployment", "Create your deployment",
"Workflow name", "Workflow name",
);
if (!text) return;
console.log(text);
app.graph.beforeChange();
var node = LiteGraph.createNode("ComfyDeploy");
node.configure({
widgets_values: [text],
});
node.pos = [0, 0];
app.graph.add(node);
app.graph.afterChange();
deployMeta = [node];
}
const deployMetaNode = deployMeta[0];
const workflow_name = deployMetaNode.widgets[0].value;
const workflow_id = deployMetaNode.widgets[1].value;
const ok = await confirmDialog.confirm(
`Confirm deployment`,
`
<div>
A new version of <button style="font-size: 18px;">${workflow_name}</button> will be deployed, do you confirm?
<br><br>
<button style="font-size: 18px;">${displayName}</button>
<br>
<button style="font-size: 18px;">${endpoint}</button>
<br><br>
<label>
<input id="include-deps" type="checkbox" checked>Include dependency</input>
</label>
<br>
<label>
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label>
</div>
`,
); );
if (!ok) return; 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>
const prompt = await app.graphToPrompt(); <br><br>
let deps = undefined; <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 (includeDeps) { const includeDeps = document.getElementById("include-deps").checked;
loadingDialog.showLoading("Fetching existing version"); const reuseHash = document.getElementById("reuse-hash").checked;
const existing_workflow = await fetch( if (endpoint.endsWith("/")) {
endpoint + "/api/workflow/" + workflow_id, endpoint = endpoint.slice(0, -1);
{ }
method: "GET", loadingDialog.showLoading("Generating snapshot");
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
},
)
.then((x) => x.json())
.catch(() => {
return {};
});
loadingDialog.close(); const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
// console.log(snapshot);
loadingDialog.close();
loadingDialog.showLoading("Generating dependency graph"); if (!snapshot) {
deps = await generateDependencyGraph({ showError(
workflow_api: prompt.output, "Error when deploying",
snapshot: snapshot, "Unable to generate snapshot, please install ComfyUI Manager",
computeFileHash: async (file) => { );
console.log(existing_workflow?.dependencies?.models); return;
}
// Match previous hash for models const title = deploy.querySelector("#button-title");
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) => { const prompt = await app.graphToPrompt();
console.log(file, x.name); let deps = undefined;
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 (includeDeps) {
if ( loadingDialog.showLoading("Fetching existing version");
!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 existing_workflow = await fetch(
endpoint + "/api/workflow/" + workflow_id,
const depsOk = await confirmDialog.confirm( {
"Check dependencies", method: "GET",
// JSON.stringify(deps, null, 2),
`
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
<iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
JSON.stringify(deps),
)}" />`,
// createDynamicUIHtml(deps),
);
if (!depsOk) return;
console.log(deps);
}
loadingDialog.showLoading("Deploying...");
title.innerText = "Deploying...";
title.style.color = "orange";
// console.log(prompt);
// TODO trim the ending / from endpoint is there is
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
// console.log(prompt.workflow);
const apiRoute = endpoint + "/api/workflow";
// const userId = apiKey
try {
const body = {
workflow_name,
workflow_id,
workflow: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
dependencies: deps,
};
console.log(body);
let data = await fetch(apiRoute, {
method: "POST",
body: JSON.stringify(body),
headers: { 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");