Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98c1aca606 | ||
|
|
09eab3202f | ||
|
|
f66adfa04f | ||
|
|
0ff751b60f | ||
|
|
a4e3bc2571 | ||
|
|
bf8e2a17b9 | ||
|
|
60c93b65f4 | ||
|
|
5d2ade4fd4 | ||
|
|
1c0a4d5950 | ||
|
|
1a8e089c7d | ||
|
|
557799cdb1 | ||
|
|
d5f52131db | ||
|
|
755399a255 | ||
|
|
ee3717b4b3 | ||
|
|
c13828af56 | ||
|
|
ffda951cfa | ||
|
|
ac0d8ba725 | ||
|
|
31c38ddb9b | ||
|
|
2f2c63fa11 | ||
|
|
7675c5ba90 | ||
|
|
7d65d23f84 | ||
|
|
a91edf6371 | ||
|
|
967709dd07 | ||
|
|
07a827d56b | ||
|
|
7f6eea361e | ||
|
|
14829a2b12 | ||
|
|
03c4e2d85d | ||
|
|
bad8b1104f | ||
|
|
ac8779dc54 | ||
|
|
2a5223f2f0 | ||
|
|
f02aef4cb3 |
@@ -2,6 +2,10 @@
|
||||
|
||||
Open source comfyui deployment platform, a `vercel` for generative workflow infra. (serverless hosted gpu with vertical intergation with comfyui)
|
||||
|
||||
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]
|
||||
> Im looking for creative hacker to join ComfyDeploy's core team! DM me on [twitter](https://x.com/BennyKokMusic)
|
||||
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -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)",
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import numpy as np
|
||||
from PIL import Image
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
import folder_paths
|
||||
from comfy.cli_args import args
|
||||
|
||||
|
||||
class ComfyDeployOutputImage:
|
||||
@@ -64,12 +65,14 @@ class ComfyDeployOutputImage:
|
||||
for batch_number, image in enumerate(images):
|
||||
i = 255.0 * image.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
metadata = PngInfo()
|
||||
if prompt is not None:
|
||||
metadata.add_text("prompt", json.dumps(prompt))
|
||||
if extra_pnginfo is not None:
|
||||
for x in extra_pnginfo:
|
||||
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
||||
metadata = None
|
||||
if not args.disable_metadata:
|
||||
metadata = PngInfo()
|
||||
if prompt is not None:
|
||||
metadata.add_text("prompt", json.dumps(prompt))
|
||||
if extra_pnginfo is not None:
|
||||
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))
|
||||
file = f"{filename_with_batch_num}_{counter:05}_.{file_type}"
|
||||
|
||||
@@ -92,7 +92,7 @@ class ComfyDeployOutputText:
|
||||
}
|
||||
)
|
||||
|
||||
return {"ui": {"text_file": results, "text": [text]}}
|
||||
return {"ui": {"text_file": results}}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"ComfyDeployOutputText": ComfyDeployOutputText}
|
||||
|
||||
+734
-116
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ class Status(Enum):
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
UPLOADING = "uploading"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class StreamingPrompt(BaseModel):
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-deploy"
|
||||
description = "Open source comfyui deployment platform, a vercel for generative workflow infra."
|
||||
version = "2.1.0"
|
||||
version = "2.3.10"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
|
||||
|
||||
|
||||
+1063
-398
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
// Snapshot Utilities
|
||||
// Centralized snapshot fetching with ComfyUI version fallback
|
||||
|
||||
/**
|
||||
* Fetches the current snapshot with ComfyUI version fallback
|
||||
* If the snapshot response has null comfyui field, it will fetch the latest ComfyUI version
|
||||
* and update the snapshot with the comfyui_hash
|
||||
*
|
||||
* @param {Function} getDataFn - Function that returns { apiKey, apiUrl } for ComfyUI version API calls
|
||||
* @returns {Promise<Object>} - The snapshot data with comfyui field populated
|
||||
*/
|
||||
export async function fetchSnapshot(getDataFn = null) {
|
||||
try {
|
||||
// Fetch the current snapshot
|
||||
const response = await fetch("/snapshot/get_current");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Snapshot fetch failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const snapshot = await response.json();
|
||||
|
||||
// Check if comfyui field is null and we have getDataFn for fallback
|
||||
if (snapshot.comfyui === null && getDataFn) {
|
||||
console.log(
|
||||
"ComfyUI version is null in snapshot, fetching latest version..."
|
||||
);
|
||||
|
||||
try {
|
||||
const data = getDataFn();
|
||||
if (data && data.apiKey) {
|
||||
const comfyuiVersionResponse = await fetch(
|
||||
`/comfyui-deploy/comfyui-version?api_url=${encodeURIComponent(
|
||||
data.apiUrl || "https://api.comfydeploy.com"
|
||||
)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${data.apiKey}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (comfyuiVersionResponse.ok) {
|
||||
const versionData = await comfyuiVersionResponse.json();
|
||||
if (versionData.comfyui_hash) {
|
||||
console.log(
|
||||
`Using ComfyUI hash from API: ${versionData.comfyui_hash}`
|
||||
);
|
||||
snapshot.comfyui = versionData.comfyui_hash;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Failed to fetch ComfyUI version from API:",
|
||||
comfyuiVersionResponse.status
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error fetching ComfyUI version fallback:", error);
|
||||
// Continue with original snapshot even if fallback fails
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
console.error("Error fetching snapshot:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
+957
-3
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user