Compare commits

..
Author SHA1 Message Date
webcoderz 4d53aa794a Update Dockerfile 2024-04-11 00:15:05 -04:00
webcoderz 12175b3955 remove apt deletion 2024-04-10 22:55:05 -04:00
webcoderz 2bcc71d24c various fixes and getting closer to parity with main 2024-04-10 20:38:24 -04:00
webcoderz 9484cb9b93 Update docker-compose.yaml
adding Postgres port env var
2024-04-10 18:17:49 -04:00
webcoderz a56ef1b06f adding local docker compose with local postgres 2024-03-28 11:15:46 -04:00
10 changed files with 449 additions and 569 deletions
-85
View File
@@ -1,85 +0,0 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
import json
import comfy
class ComfyUIDeployExternalImageBatch:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_images"},
),
"images": (
"STRING",
{"multiline": False, "default": "[]"},
),
},
"optional": {
"default_value": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "run"
CATEGORY = "image"
def run(self, input_id, images=None, default_value=None):
processed_images = []
try:
images_list = json.loads(images) # Assuming images is a JSON array string
print(images_list)
for img_input in images_list:
if img_input.startswith('http'):
import requests
from io import BytesIO
print("Fetching image from url: ", img_input)
response = requests.get(img_input)
image = Image.open(BytesIO(response.content))
elif img_input.startswith('data:image/png;base64,') or img_input.startswith('data:image/jpeg;base64,') or img_input.startswith('data:image/jpg;base64,'):
import base64
from io import BytesIO
print("Decoding base64 image")
base64_image = img_input[img_input.find(",")+1:]
decoded_image = base64.b64decode(base64_image)
image = Image.open(BytesIO(decoded_image))
else:
raise ValueError("Invalid image url or base64 data provided.")
image = ImageOps.exif_transpose(image)
image = image.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image)[None,]
processed_images.append(image_tensor)
except Exception as e:
print(f"Error processing images: {e}")
pass
if default_value is not None and len(images_list) == 0:
processed_images.append(default_value) # Assuming default_value is a pre-processed image tensor
# Resize images if necessary and concatenate from MakeImageBatch in ImpactPack
if processed_images:
base_shape = processed_images[0].shape[1:] # Get the shape of the first image for comparison
batch_tensor = processed_images[0]
for i in range(1, len(processed_images)):
if processed_images[i].shape[1:] != base_shape:
# Resize to match the first image's dimensions
processed_images[i] = comfy.utils.common_upscale(processed_images[i].movedim(-1, 1), base_shape[1], base_shape[0], "lanczos", "center").movedim(1, -1)
batch_tensor = torch.cat((batch_tensor, processed_images[i]), dim=0)
# Concatenate using torch.cat
else:
batch_tensor = None # or handle the empty case as needed
return (batch_tensor, )
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalImageBatch": ComfyUIDeployExternalImageBatch}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalImageBatch": "External Image Batch (ComfyUI Deploy)"}
+1 -8
View File
@@ -32,12 +32,7 @@ class ComfyUIDeployExternalLora:
import os
import uuid
print('external lora using')
print("input id: ", input_id)
print("default lora : ", default_lora_name)
if input_id:
if input_id.startswith('http'):
if input_id and input_id.startswith('http'):
unique_filename = str(uuid.uuid4()) + ".safetensors"
print(unique_filename)
print(folder_paths.folder_names_and_paths["loras"][0][0])
@@ -49,8 +44,6 @@ class ComfyUIDeployExternalLora:
out_file.write(response.content)
return (unique_filename,)
else:
return (input_id,)
return (default_lora_name,)
+63 -95
View File
@@ -13,6 +13,7 @@ import traceback
import uuid
import asyncio
import logging
from enum import Enum
from urllib.parse import quote
import threading
import hashlib
@@ -23,11 +24,30 @@ from PIL import Image
import copy
import struct
from globals import StreamingPrompt, Status, sockets, SimplePrompt, streaming_prompt_metadata, prompt_metadata
from globals import StreamingPrompt, sockets, streaming_prompt_metadata, BaseModel
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_task = None
prompt_metadata: dict[str, SimplePrompt] = {}
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'
@@ -106,38 +126,6 @@ def apply_random_seed_to_workflow(workflow_api):
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 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
def send_prompt(sid: str, inputs: StreamingPrompt):
# workflow_api = inputs.workflow_api
workflow_api = copy.deepcopy(inputs.workflow_api)
@@ -147,7 +135,30 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
print("getting inputs" , inputs.inputs)
apply_inputs_to_workflow(workflow_api, inputs.inputs, sid=sid)
# Loop through each of the inputs and replace them
for key, value in workflow_api.items():
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:
new_value = inputs.inputs[value['inputs']['input_id']]
# Lets skip it if its an image
if isinstance(new_value, Image.Image):
continue
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
print(workflow_api)
@@ -180,15 +191,15 @@ async def comfy_deploy_run(request):
prompt_server = server.PromptServer.instance
data = await request.json()
# In older version, we use workflow_api, but this has inputs already swapped in nextjs frontend, which is tricky
workflow_api = data.get("workflow_api_raw")
workflow_api = data.get("workflow_api")
# The prompt id generated from comfy deploy, can be None
prompt_id = data.get("prompt_id")
inputs = data.get("inputs")
# Now it handles directly in here
apply_random_seed_to_workflow(workflow_api)
apply_inputs_to_workflow(workflow_api, inputs)
# for key in workflow_api:
# if 'inputs' in workflow_api[key] and 'seed' in workflow_api[key]['inputs']:
# workflow_api[key]['inputs']['seed'] = randomSeed()
prompt = {
"prompt": workflow_api,
@@ -371,58 +382,26 @@ async def upload_file_endpoint(request):
}, 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')
async def get_file_hash(request):
file_path = request.rel_url.query.get('file_path', '')
if not file_path:
if file_path is None:
return web.json_response({
"error": "file_path is required"
}, status=400)
try:
base = folder_paths.base_path
full_file_path = os.path.join(base, file_path)
# Check if the file hash is in the cache
if full_file_path in file_hash_cache:
file_hash = file_hash_cache[full_file_path]
else:
start_time = time.time()
file_hash = await compute_sha256_checksum(full_file_path)
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()
file_path = os.path.join(base, file_path)
# print("file_path", file_path)
start_time = time.time() # Capture the start time
file_hash = await compute_sha256_checksum(
file_path
)
end_time = time.time() # Capture the end time after the code execution
elapsed_time = end_time - start_time # Calculate the elapsed time
print(f"Execution time: {elapsed_time} seconds")
return web.json_response({
"file_hash": file_hash
})
@@ -674,15 +653,6 @@ async def send_json_override(self, event, data, sid=None):
# await update_run_with_output(prompt_id, 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'))
# update_run_with_output(prompt_id, data.get('output'))
@@ -726,7 +696,7 @@ def update_run(prompt_id: str, status: Status):
if (prompt_metadata[prompt_id].status != status):
# when the status is already failed, we don't want to update it to success
if (prompt_metadata[prompt_id].status is Status.FAILED):
if ('status' in prompt_metadata[prompt_id] and prompt_metadata[prompt_id].status == Status.FAILED):
return
status_endpoint = prompt_metadata[prompt_id].status_endpoint
@@ -922,9 +892,6 @@ 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):
items = data.get(key, [])
for item in items:
# # Skipping temp files
if item.get("type") == "temp":
continue
await upload_file(
prompt_id,
item.get("filename"),
@@ -933,6 +900,7 @@ 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)
)
# Upload files in the background
async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=True):
try:
+55
View File
@@ -0,0 +1,55 @@
version: '3.9'
services:
comfy-deploy:
build:
context: .
dockerfile: ./local/Dockerfile
restart: unless-stopped
volumes:
- ./local/scripts/entrypoint.sh:/comfyui-deploy/web/deploy_entrypoint.sh
entrypoint: /comfyui-deploy/web/deploy_entrypoint.sh
ports:
- 3000:3000
depends_on:
- postgres
- pg_proxy
- localstack
environment:
VSCODE_DEV_CONTAINER: true
### comfy-deploy services
postgres:
image: "postgres:15.2-alpine"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: verceldb
POSTGRES_PORT: 5480
expose:
- 5480
pg_proxy:
image: ghcr.io/neondatabase/wsproxy:latest
environment:
APPEND_PORT: "postgres:5480"
ALLOW_ADDR_REGEX: ".*"
LOG_TRAFFIC: "true"
expose:
- 80
depends_on:
- postgres
localstack:
image: localstack/localstack:latest
environment:
SERVICES: s3
ports:
- 4566:4566
volumes:
- ../localstack/aws:/etc/localstack/init/ready.d
- ../localstack/aws:/app/web/aws
+3 -21
View File
@@ -1,22 +1,17 @@
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
@@ -25,20 +20,7 @@ class StreamingPrompt(BaseModel):
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:
+18
View File
@@ -0,0 +1,18 @@
FROM node:21-bullseye AS comfy_deploy
RUN apt-get update && apt-get install -y python3 make g++
RUN npm install -g bun
COPY ./web /web
WORKDIR /web
RUN cp .env.example .env.local
RUN bunx node-gyp
RUN bun i
ENTRYPOINT [ "bun", "dev" ]
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
echo "comfy deploy container starting.."
echo "Running migrations.."
bun migrate-local
echo "Starting comfy deploy.."
bun dev
-6
View File
@@ -58,9 +58,6 @@ if cd_enable_log:
print("** Comfy Deploy logging enabled")
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__))
@@ -70,6 +67,3 @@ 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)
+11 -65
View File
@@ -1,18 +1,10 @@
import { app } from "./app.js";
import { api } from "./api.js";
import { ComfyWidgets, LGraphNode } from "./widgets.js";
import { generateDependencyGraph } from "https://esm.sh/[email protected]5";
import { generateDependencyGraph } from "https://esm.sh/[email protected]2";
const loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
function sendEventToCD(event, data) {
const message = {
type: event,
data: data,
};
window.parent.postMessage(JSON.stringify(message), "*");
}
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
/** @type {ComfyExtension} */
const ext = {
@@ -26,11 +18,6 @@ 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";
}
const data = getData();
let endpoint = data.endpoint;
@@ -165,32 +152,9 @@ const ext = {
async setup() {
// const graphCanvas = document.getElementById("graph-canvas");
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);
}
} catch (error) {
// console.error("Error processing message:", error);
}
// if (!event.data.flow || Object.entries(event.data.flow).length <= 0)
// return;
window.addEventListener("message", (event) => {
if (!event.data.flow || Object.entries(event.data.flow).length <= 0)
return;
// updateBlendshapesPrompts(event.data.flow);
});
@@ -203,17 +167,6 @@ const ext = {
// }
});
app.graph.onAfterChange = ((originalFunction) => async function () {
const prompt = await app.graphToPrompt();
sendEventToCD("cd_plugin_onAfterChange", prompt);
if (typeof originalFunction === "function") {
originalFunction.apply(this, arguments);
}
})(app.graph.onAfterChange);
sendEventToCD("cd_plugin_setup");
},
};
@@ -314,9 +267,14 @@ function createDynamicUIHtml(data) {
return html;
}
async function deployWorkflow() {
const deploy = document.getElementById("deploy-button");
function addButton() {
const menu = document.querySelector(".comfy-menu");
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;
@@ -597,18 +555,6 @@ async function deployWorkflow() {
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");
+2 -2
View File
@@ -9,10 +9,10 @@ if (process.env.VERCEL_ENV !== "production") {
// Set the WebSocket proxy to work with the local instance
if (isDevContainer) {
// Running inside a VS Code devcontainer
neonConfig.wsProxy = (host) => "host.docker.internal:5481/v1";
neonConfig.wsProxy = (host) => "pg_proxy:80/v1";
} else {
// Not running inside a VS Code devcontainer
neonConfig.wsProxy = (host) => `${host}:5481/v1`;
neonConfig.wsProxy = (host) => "pg_proxy:80/v1";
}
// Disable all authentication and encryption
neonConfig.useSecureWebSocket = false;