Compare commits

..
Author SHA1 Message Date
KarrixLee ebc71a7d72 refactor 2025-09-22 18:13:54 +08:00
KarrixLee 9076b4c0ce set to true 2025-09-22 18:08:52 +08:00
KarrixLee e34d1651f7 fix: remove metadata in output node 2025-09-22 18:05:56 +08:00
BennyKok 1c0a4d5950 Update README with additional resources and recruitment note 2025-09-17 07:05:14 -07:00
bennykok 1a8e089c7d version bump 2025-09-12 10:37:45 -07:00
bennykok 557799cdb1 fix 2025-09-11 23:56:33 -07:00
bennykok d5f52131db fix 2025-09-11 23:53:26 -07:00
bennykok 755399a255 fix: symlink 2025-09-11 23:44:38 -07:00
bennykok ee3717b4b3 any file + psd psb support 2025-09-11 23:31:28 -07:00
KarrixLee c13828af56 fix: comfy manager error, bump to v2.3.6 2025-09-11 04:13:36 +08:00
9 changed files with 332 additions and 35 deletions
+2
View File
@@ -4,6 +4,8 @@ Open source comfyui deployment platform, a `vercel` for generative workflow infr
Check out our latest lcoal demo -> https://github.com/comfy-deploy/comfyui-api-comfydeploy Check out our latest lcoal demo -> https://github.com/comfy-deploy/comfyui-api-comfydeploy
Full backend and frontend is here -> https://github.com/comfy-deploy/comfydeploy
> [!NOTE] > [!NOTE]
> Im looking for creative hacker to join ComfyDeploy's core team! DM me on [twitter](https://x.com/BennyKokMusic) > Im looking for creative hacker to join ComfyDeploy's core team! DM me on [twitter](https://x.com/BennyKokMusic)
+137
View File
@@ -0,0 +1,137 @@
import folder_paths
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyUIDeployExternalFile:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_file"},
),
},
"optional": {
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
"file_url": (
"STRING",
{"multiline": False, "default": ""},
),
},
}
RETURN_TYPES = (WILDCARD,)
RETURN_NAMES = ("path",)
FUNCTION = "run"
CATEGORY = "🔗ComfyDeploy"
def run(
self,
input_id,
display_name=None,
description=None,
file_url=None,
):
import requests
import os
import uuid
from urllib.parse import urlparse
if file_url:
if file_url.startswith("http"):
# Use cache directory for saving files
cache_dir = folder_paths.get_temp_directory()
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
# Always generate random filename to avoid conflicts
parsed_url = urlparse(file_url)
original_filename = os.path.basename(parsed_url.path)
# Extract file extension from original filename if available
file_extension = ""
if original_filename and "." in original_filename:
file_extension = os.path.splitext(original_filename)[1]
else:
# Try to determine extension from content-type if no extension found
file_extension = ".bin"
# Generate random filename with preserved extension
filename = str(uuid.uuid4()) + file_extension
destination_path = os.path.join(cache_dir, filename)
print(f"Cache directory: {cache_dir}")
print(f"Destination path: {destination_path}")
print(
"Downloading external file - "
+ file_url
+ " to "
+ destination_path
)
headers = {"User-Agent": "Mozilla/5.0"}
try:
response = requests.get(
file_url,
headers=headers,
allow_redirects=True,
timeout=30, # Add timeout to prevent hanging
)
response.raise_for_status()
with open(destination_path, "wb") as out_file:
out_file.write(response.content)
print(f"External file downloaded: {file_url} to {destination_path}")
return (destination_path,)
except requests.exceptions.HTTPError as e:
error_msg = f"HTTP Error {e.response.status_code}: {e.response.reason} for URL: {file_url}"
print(f"⚠️ Download failed - {error_msg}")
if e.response.status_code == 404:
print(
"💡 This URL might have expired or the file may have been deleted"
)
# Return empty string instead of crashing
return ("",)
except requests.exceptions.RequestException as e:
error_msg = (
f"Network error downloading file from {file_url}: {str(e)}"
)
print(f"⚠️ Download failed - {error_msg}")
return ("",)
except Exception as e:
error_msg = (
f"Unexpected error downloading file from {file_url}: {str(e)}"
)
print(f"⚠️ Download failed - {error_msg}")
return ("",)
else:
print(f"External file loading: {file_url}")
return (file_url,)
else:
print(f"No file URL provided")
return ("",)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalFile": ComfyUIDeployExternalFile}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyUIDeployExternalFile": "External File (ComfyUI Deploy)"
}
+168
View File
@@ -0,0 +1,168 @@
import folder_paths
import os
import shutil
import uuid
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyDeployOutputFile:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"file_path": (
"STRING",
{
"forceInput": True,
"tooltip": "Path to the file to output and upload.",
},
),
},
"optional": {
"output_id": (
"STRING",
{"multiline": False, "default": "output_file"},
),
},
}
RETURN_TYPES = ()
FUNCTION = "run"
OUTPUT_NODE = True
CATEGORY = "🔗ComfyDeploy"
DESCRIPTION = "Outputs any file by path for upload to ComfyDeploy."
def run(self, file_path, output_id="output_file"):
if not file_path or not os.path.exists(file_path):
print(f"⚠️ File not found: {file_path}")
return {"ui": {"files": []}}
# Security checks - ensure file is within safe ComfyUI paths
try:
# Get absolute paths for comparison
file_abs_path = os.path.abspath(file_path)
base_path = folder_paths.base_path
temp_dir = folder_paths.get_temp_directory()
# Check if file is within ComfyUI base path or temp directory
if not (
file_abs_path.startswith(os.path.abspath(base_path))
or file_abs_path.startswith(os.path.abspath(temp_dir))
):
print(f"⚠️ Security: File outside allowed ComfyUI paths: {file_path}")
return {"ui": {"files": []}}
# Check for path traversal attempts (but allow absolute paths within ComfyUI)
if ".." in file_path:
print(f"⚠️ Security: Path traversal attempt detected: {file_path}")
return {"ui": {"files": []}}
except Exception as e:
print(f"⚠️ Security check failed: {str(e)}")
return {"ui": {"files": []}}
# Get the original filename and extension
original_filename = os.path.basename(file_path)
file_extension = os.path.splitext(original_filename)[1]
# Additional filename security check
if ".." in original_filename:
print(f"⚠️ Security: Insecure filename: {original_filename}")
return {"ui": {"files": []}}
results = []
# Check if file is in output folder, if not, symlink it there
try:
if file_path.startswith(self.output_dir):
# File is already in output directory - use as is
relative_path = os.path.relpath(file_path, self.output_dir)
path_parts = relative_path.split(os.sep)
if len(path_parts) > 1:
subfolder = os.sep.join(path_parts[:-1])
else:
subfolder = ""
filename = path_parts[-1]
file_type = self.type
else:
# File is not in output folder - symlink it to output/temp
print(
f"File is not in output folder, symlinking to output/temp: {file_path}"
)
output_temp_dir = os.path.join(self.output_dir, "temp")
if not os.path.exists(output_temp_dir):
os.makedirs(output_temp_dir)
# Use the existing filename but with UUID prefix to avoid conflicts
file_ext = os.path.splitext(original_filename)[1]
temp_filename = f"{uuid.uuid4()}{file_ext}"
temp_path = os.path.join(output_temp_dir, temp_filename)
# Create symlink to file in output/temp directory where upload system expects it
try:
# Remove existing symlink if it exists
if os.path.exists(temp_path):
os.remove(temp_path)
os.symlink(file_path, temp_path)
print(f"File symlinked to output/temp: {temp_path} -> {file_path}")
except OSError as e:
# Fall back to copying if symlink fails
print(f"Symlink failed ({e}), falling back to copy")
shutil.copy2(file_path, temp_path)
print(f"File copied to output/temp: {temp_path}")
# Use output/temp directory structure for upload
subfolder = "temp"
filename = temp_filename
file_type = self.type
results.append(
{
"filename": filename,
"subfolder": subfolder,
"type": file_type,
"output_id": output_id,
}
)
except Exception as e:
print(f"⚠️ Error processing file path: {str(e)}")
return {"ui": {"files": []}}
# Determine the appropriate UI key based on file type
file_ext = file_extension.lower()
if file_ext in [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff"]:
ui_key = "images"
elif file_ext in [".mp3", ".wav", ".flac", ".aac", ".ogg"]:
ui_key = "audio"
elif file_ext in [".txt", ".json", ".md", ".csv"]:
ui_key = "text_file"
elif file_ext in [".exr", ".hdr"]:
ui_key = "images" # EXR files are still images
elif file_ext in [".zip", ".psb", ".psd"]:
ui_key = "files" # Archives and Photoshop project files
else:
ui_key = "files" # Generic files
return {"ui": {ui_key: results}}
NODE_CLASS_MAPPINGS = {
"ComfyDeployOutputFile": ComfyDeployOutputFile,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyDeployOutputFile": "File Output (ComfyDeploy)",
}
+8 -5
View File
@@ -33,6 +33,7 @@ class ComfyDeployOutputImage:
"STRING", "STRING",
{"multiline": False, "default": "output_images"}, {"multiline": False, "default": "output_images"},
), ),
"remove_metadata": ("BOOLEAN", {"default": True}),
}, },
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
} }
@@ -53,6 +54,7 @@ class ComfyDeployOutputImage:
output_id="output_images", output_id="output_images",
prompt=None, prompt=None,
extra_pnginfo=None, extra_pnginfo=None,
remove_metadata=True,
): ):
filename_prefix += self.prefix_append filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = ( full_output_folder, filename, counter, subfolder, filename_prefix = (
@@ -65,11 +67,12 @@ class ComfyDeployOutputImage:
i = 255.0 * image.cpu().numpy() i = 255.0 * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = PngInfo() metadata = PngInfo()
if prompt is not None: if not remove_metadata:
metadata.add_text("prompt", json.dumps(prompt)) if prompt is not None:
if extra_pnginfo is not None: metadata.add_text("prompt", json.dumps(prompt))
for x in extra_pnginfo: if extra_pnginfo is not None:
metadata.add_text(x, json.dumps(extra_pnginfo[x])) for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.{file_type}" file = f"{filename_with_batch_num}_{counter:05}_.{file_type}"
+15
View File
@@ -516,6 +516,9 @@ def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
if value["class_type"] == "ComfyUIDeployExternalEXR": if value["class_type"] == "ComfyUIDeployExternalEXR":
value["inputs"]["exr_file"] = new_value value["inputs"]["exr_file"] = new_value
if value["class_type"] == "ComfyUIDeployExternalFile":
value["inputs"]["file_url"] = new_value
if value["class_type"] == "ComfyUIDeployExternalSeed": if value["class_type"] == "ComfyUIDeployExternalSeed":
logger.info( logger.info(
f"Applied random seed {new_value} to {value['class_type']}" f"Applied random seed {new_value} to {value['class_type']}"
@@ -896,6 +899,10 @@ async def upload_file_endpoint(request):
file_type = "image/png" file_type = "image/png"
elif file_extension == ".webp": elif file_extension == ".webp":
file_type = "image/webp" file_type = "image/webp"
elif file_extension == ".zip":
file_type = "application/zip"
elif file_extension in [".psd", ".psb"]:
file_type = "image/vnd.adobe.photoshop"
else: else:
file_type = ( file_type = (
"application/octet-stream" # Default to binary file type if unknown "application/octet-stream" # Default to binary file type if unknown
@@ -2098,6 +2105,10 @@ async def handle_upload(
file_type = "image/png" file_type = "image/png"
elif file_extension == ".webp": elif file_extension == ".webp":
file_type = "image/webp" file_type = "image/webp"
elif file_extension == ".zip":
file_type = "application/zip"
elif file_extension in [".psd", ".psb"]:
file_type = "image/vnd.adobe.photoshop"
upload_tasks.append( upload_tasks.append(
upload_file( upload_file(
@@ -2563,6 +2574,10 @@ class UploadQueue:
content_type = "image/webp" content_type = "image/webp"
elif file_extension == ".gif": elif file_extension == ".gif":
content_type = "image/gif" content_type = "image/gif"
elif file_extension == ".zip":
content_type = "application/zip"
elif file_extension in [".psd", ".psb"]:
content_type = "image/vnd.adobe.photoshop"
else: else:
content_type = file_info.get("content_type", "application/octet-stream") content_type = file_info.get("content_type", "application/octet-stream")
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-deploy" name = "comfyui-deploy"
description = "Open source comfyui deployment platform, a vercel for generative workflow infra." description = "Open source comfyui deployment platform, a vercel for generative workflow infra."
version = "2.3.5" version = "2.3.7"
license = { file = "LICENSE" } license = { file = "LICENSE" }
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"] dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
-14
View File
@@ -10,7 +10,6 @@ import {
} from "./workflow-list.js"; } from "./workflow-list.js";
import { initializeMachineManager } from "./machine-manager.js"; import { initializeMachineManager } from "./machine-manager.js";
import { initializeModelManager } from "./model-manager.js"; import { initializeModelManager } from "./model-manager.js";
import { fetchSnapshotSimple } from "./snapshot-utils.js";
const styles = ` const styles = `
.comfydeploy-menu-item { .comfydeploy-menu-item {
@@ -1355,19 +1354,6 @@ async function deployWorkflow() {
if (endpoint.endsWith("/")) { if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1); endpoint = endpoint.slice(0, -1);
} }
loadingDialog.showLoading("Generating snapshot");
const snapshot = await fetchSnapshotSimple();
console.log(snapshot);
loadingDialog.close();
if (!snapshot) {
showError(
"Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager"
);
return;
}
const title = deploy.querySelector("#button-title"); const title = deploy.querySelector("#button-title");
+1 -1
View File
@@ -1,7 +1,7 @@
// Machine Manager Script // Machine Manager Script
// Handles local storage machine ID functionality // Handles local storage machine ID functionality
import { fetchSnapshot, fetchSnapshotSimple } from "./snapshot-utils.js"; import { fetchSnapshot } from "./snapshot-utils.js";
const MACHINE_STORAGE_KEY = "comfy_deploy_machine_id"; const MACHINE_STORAGE_KEY = "comfy_deploy_machine_id";
-14
View File
@@ -66,17 +66,3 @@ export async function fetchSnapshot(getDataFn = null) {
throw error; throw error;
} }
} }
/**
* Simple snapshot fetch without ComfyUI version fallback
* Use this when you don't need the ComfyUI version fallback logic
*
* @returns {Promise<Object>} - The snapshot data as-is
*/
export async function fetchSnapshotSimple() {
const response = await fetch("/snapshot/get_current");
if (!response.ok) {
throw new Error(`Snapshot fetch failed: ${response.status}`);
}
return response.json();
}