Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ee7d03937 | ||
|
|
fdf1f08d8d | ||
|
|
94a5f52b3f | ||
|
|
f91680d435 | ||
|
|
5e88e7e406 | ||
|
|
7421b2ce9e | ||
|
|
eaf55eb974 | ||
|
|
4ef8e7e12b | ||
|
|
142458994e | ||
|
|
5ac4aa98a3 | ||
|
|
31c38ddb9b | ||
|
|
2f2c63fa11 | ||
|
|
7675c5ba90 | ||
|
|
7d65d23f84 | ||
|
|
a91edf6371 |
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Open source comfyui deployment platform, a `vercel` for generative workflow infra. (serverless hosted gpu with vertical intergation with comfyui)
|
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
|
||||||
|
|
||||||
> [!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)
|
||||||
|
|
||||||
|
|||||||
@@ -672,6 +672,14 @@ async def comfy_deploy_run(request):
|
|||||||
return web.json_response(res, status=status)
|
return web.json_response(res, status=status)
|
||||||
|
|
||||||
|
|
||||||
|
@server.PromptServer.instance.routes.post("/comfyui-deploy/interrupt")
|
||||||
|
async def interrupt_prompt(request):
|
||||||
|
data = await request.json()
|
||||||
|
prompt_id = data.get("prompt_id")
|
||||||
|
await update_run(prompt_id, Status.CANCELLED)
|
||||||
|
return web.json_response({"message": "Prompt interrupted"}, status=200)
|
||||||
|
|
||||||
|
|
||||||
async def stream_prompt(data, token):
|
async def stream_prompt(data, token):
|
||||||
# In older version, we use workflow_api, but this has inputs already swapped in nextjs frontend, which is tricky
|
# 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_raw")
|
||||||
@@ -3140,6 +3148,68 @@ async def get_workflow_proxy(request):
|
|||||||
return web.json_response({"error": str(e)}, status=500)
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# fetch workflow versions (infinite scroll support)
|
||||||
|
@server.PromptServer.instance.routes.get("/comfyui-deploy/workflow/versions")
|
||||||
|
async def get_workflow_versions_proxy(request):
|
||||||
|
workflow_id = request.rel_url.query.get("workflow_id")
|
||||||
|
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
|
||||||
|
search = request.rel_url.query.get("search", "")
|
||||||
|
limit = request.rel_url.query.get("limit", "20")
|
||||||
|
offset = request.rel_url.query.get("offset", "0")
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
|
if not auth_header:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Authorization header is required"}, status=401
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build target URL with query params
|
||||||
|
params = {"limit": limit, "offset": offset}
|
||||||
|
if search:
|
||||||
|
params["search"] = search
|
||||||
|
|
||||||
|
query = urlencode(params)
|
||||||
|
target_url = f"{api_url}/api/workflow/{workflow_id}/versions"
|
||||||
|
if query:
|
||||||
|
target_url += f"?{query}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ensure_client_session()
|
||||||
|
async with client_session.get(
|
||||||
|
target_url, headers={"Authorization": auth_header}
|
||||||
|
) as response:
|
||||||
|
json_data = await response.json()
|
||||||
|
return web.json_response(json_data, status=response.status)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# fetch a specific workflow version json
|
||||||
|
@server.PromptServer.instance.routes.get("/comfyui-deploy/workflow/version")
|
||||||
|
async def get_workflow_version_proxy(request):
|
||||||
|
workflow_id = request.rel_url.query.get("workflow_id")
|
||||||
|
version = request.rel_url.query.get("version")
|
||||||
|
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
|
if not auth_header:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Authorization header is required"}, status=401
|
||||||
|
)
|
||||||
|
|
||||||
|
target_url = f"{api_url}/api/workflow/{workflow_id}/version/{version}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ensure_client_session()
|
||||||
|
async with client_session.get(
|
||||||
|
target_url, headers={"Authorization": auth_header}
|
||||||
|
) as response:
|
||||||
|
json_data = await response.json()
|
||||||
|
return web.json_response(json_data, status=response.status)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
# for getting a machine by id
|
# for getting a machine by id
|
||||||
@server.PromptServer.instance.routes.get("/comfyui-deploy/machine")
|
@server.PromptServer.instance.routes.get("/comfyui-deploy/machine")
|
||||||
async def get_machine_proxy(request):
|
async def get_machine_proxy(request):
|
||||||
@@ -3282,3 +3352,292 @@ async def get_comfyui_version_proxy(request):
|
|||||||
return web.json_response(json_data, status=response.status)
|
return web.json_response(json_data, status=response.status)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return web.json_response({"error": str(e)}, status=500)
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# Proxy: generate single-part upload URL
|
||||||
|
@server.PromptServer.instance.routes.post(
|
||||||
|
"/comfyui-deploy/volume/file/generate-upload-url"
|
||||||
|
)
|
||||||
|
async def proxy_generate_upload_url(request):
|
||||||
|
data = await request.json()
|
||||||
|
api_url = data.get("api_url", "https://api.comfydeploy.com")
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
|
if not auth_header:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Authorization header is required"}, status=401
|
||||||
|
)
|
||||||
|
|
||||||
|
target_url = f"{api_url}/api/volume/file/generate-upload-url"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ensure_client_session()
|
||||||
|
async with client_session.post(
|
||||||
|
target_url,
|
||||||
|
json={
|
||||||
|
"filename": data.get("filename"),
|
||||||
|
"contentType": data.get("contentType"),
|
||||||
|
"size": data.get("size"),
|
||||||
|
},
|
||||||
|
headers={"Authorization": auth_header},
|
||||||
|
) as response:
|
||||||
|
json_data = await response.json()
|
||||||
|
return web.json_response(json_data, status=response.status)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# Proxy: initiate multipart upload
|
||||||
|
@server.PromptServer.instance.routes.post(
|
||||||
|
"/comfyui-deploy/volume/file/initiate-multipart-upload"
|
||||||
|
)
|
||||||
|
async def proxy_initiate_multipart_upload(request):
|
||||||
|
data = await request.json()
|
||||||
|
api_url = data.get("api_url", "https://api.comfydeploy.com")
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
|
if not auth_header:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Authorization header is required"}, status=401
|
||||||
|
)
|
||||||
|
|
||||||
|
target_url = f"{api_url}/api/volume/file/initiate-multipart-upload"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ensure_client_session()
|
||||||
|
async with client_session.post(
|
||||||
|
target_url,
|
||||||
|
json={
|
||||||
|
"filename": data.get("filename"),
|
||||||
|
"contentType": data.get("contentType"),
|
||||||
|
"size": data.get("size"),
|
||||||
|
},
|
||||||
|
headers={"Authorization": auth_header},
|
||||||
|
) as response:
|
||||||
|
json_data = await response.json()
|
||||||
|
return web.json_response(json_data, status=response.status)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# Proxy: generate part upload URL
|
||||||
|
@server.PromptServer.instance.routes.post(
|
||||||
|
"/comfyui-deploy/volume/file/generate-part-upload-url"
|
||||||
|
)
|
||||||
|
async def proxy_generate_part_upload_url(request):
|
||||||
|
data = await request.json()
|
||||||
|
api_url = data.get("api_url", "https://api.comfydeploy.com")
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
|
if not auth_header:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Authorization header is required"}, status=401
|
||||||
|
)
|
||||||
|
|
||||||
|
target_url = f"{api_url}/api/volume/file/generate-part-upload-url"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ensure_client_session()
|
||||||
|
async with client_session.post(
|
||||||
|
target_url,
|
||||||
|
json={
|
||||||
|
"uploadId": data.get("uploadId"),
|
||||||
|
"key": data.get("key"),
|
||||||
|
"partNumber": data.get("partNumber"),
|
||||||
|
},
|
||||||
|
headers={"Authorization": auth_header},
|
||||||
|
) as response:
|
||||||
|
json_data = await response.json()
|
||||||
|
return web.json_response(json_data, status=response.status)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# Proxy: complete multipart upload
|
||||||
|
@server.PromptServer.instance.routes.post(
|
||||||
|
"/comfyui-deploy/volume/file/complete-multipart-upload"
|
||||||
|
)
|
||||||
|
async def proxy_complete_multipart_upload(request):
|
||||||
|
data = await request.json()
|
||||||
|
api_url = data.get("api_url", "https://api.comfydeploy.com")
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
|
if not auth_header:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Authorization header is required"}, status=401
|
||||||
|
)
|
||||||
|
|
||||||
|
target_url = f"{api_url}/api/volume/file/complete-multipart-upload"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ensure_client_session()
|
||||||
|
async with client_session.post(
|
||||||
|
target_url,
|
||||||
|
json={
|
||||||
|
"uploadId": data.get("uploadId"),
|
||||||
|
"key": data.get("key"),
|
||||||
|
"parts": data.get("parts"),
|
||||||
|
},
|
||||||
|
headers={"Authorization": auth_header},
|
||||||
|
) as response:
|
||||||
|
json_data = await response.json()
|
||||||
|
return web.json_response(json_data, status=response.status)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# Proxy: abort multipart upload
|
||||||
|
@server.PromptServer.instance.routes.post(
|
||||||
|
"/comfyui-deploy/volume/file/abort-multipart-upload"
|
||||||
|
)
|
||||||
|
async def proxy_abort_multipart_upload(request):
|
||||||
|
data = await request.json()
|
||||||
|
api_url = data.get("api_url", "https://api.comfydeploy.com")
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
|
if not auth_header:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Authorization header is required"}, status=401
|
||||||
|
)
|
||||||
|
|
||||||
|
target_url = f"{api_url}/api/volume/file/abort-multipart-upload"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ensure_client_session()
|
||||||
|
async with client_session.post(
|
||||||
|
target_url,
|
||||||
|
json={
|
||||||
|
"uploadId": data.get("uploadId"),
|
||||||
|
"key": data.get("key"),
|
||||||
|
},
|
||||||
|
headers={"Authorization": auth_header},
|
||||||
|
) as response:
|
||||||
|
json_data = await response.json()
|
||||||
|
return web.json_response(json_data, status=response.status)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# Proxy: add model (unified endpoint)
|
||||||
|
@server.PromptServer.instance.routes.post("/comfyui-deploy/volume/model")
|
||||||
|
async def proxy_add_model(request):
|
||||||
|
data = await request.json()
|
||||||
|
api_url = data.get("api_url", "https://api.comfydeploy.com")
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
|
if not auth_header:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Authorization header is required"}, status=401
|
||||||
|
)
|
||||||
|
|
||||||
|
target_url = f"{api_url}/api/volume/model"
|
||||||
|
|
||||||
|
# pass body through but remove api_url key
|
||||||
|
forward_body = dict(data)
|
||||||
|
if "api_url" in forward_body:
|
||||||
|
forward_body.pop("api_url")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ensure_client_session()
|
||||||
|
async with client_session.post(
|
||||||
|
target_url, json=forward_body, headers={"Authorization": auth_header}
|
||||||
|
) as response:
|
||||||
|
json_data = await response.json()
|
||||||
|
return web.json_response(json_data, status=response.status)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# FS: stat file (size)
|
||||||
|
@server.PromptServer.instance.routes.get("/comfyui-deploy/fs/stat")
|
||||||
|
async def fs_stat(request):
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
|
||||||
|
file_path = request.rel_url.query.get("path")
|
||||||
|
if not file_path:
|
||||||
|
return web.json_response({"error": "path is required"}, status=400)
|
||||||
|
|
||||||
|
# Basic safeguard: ensure it's a ComfyUI models path
|
||||||
|
if "/models/" not in file_path:
|
||||||
|
return web.json_response({"error": "invalid path"}, status=400)
|
||||||
|
|
||||||
|
st = os.stat(file_path)
|
||||||
|
return web.json_response({"size": st.st_size})
|
||||||
|
except FileNotFoundError:
|
||||||
|
return web.json_response({"error": "not found"}, status=404)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# Upload a multipart part directly from machine filesystem to S3 upload URL
|
||||||
|
@server.PromptServer.instance.routes.post(
|
||||||
|
"/comfyui-deploy/volume/file/upload-part-from-path"
|
||||||
|
)
|
||||||
|
async def upload_part_from_path(request):
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
import io
|
||||||
|
|
||||||
|
data = await request.json()
|
||||||
|
file_path = data.get("filePath")
|
||||||
|
upload_url = data.get("uploadUrl")
|
||||||
|
start = int(data.get("start", 0))
|
||||||
|
end = int(data.get("end", 0))
|
||||||
|
if not file_path or not upload_url:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "filePath and uploadUrl are required"}, status=400
|
||||||
|
)
|
||||||
|
if "/models/" not in file_path:
|
||||||
|
return web.json_response({"error": "invalid filePath"}, status=400)
|
||||||
|
if end <= start:
|
||||||
|
return web.json_response({"error": "invalid byte range"}, status=400)
|
||||||
|
|
||||||
|
size = end - start
|
||||||
|
|
||||||
|
await ensure_client_session()
|
||||||
|
|
||||||
|
# Important: S3 pre-signed part uploads do not support chunked transfer
|
||||||
|
# Buffer the exact part into memory to provide a Content-Length header
|
||||||
|
buffer = bytearray()
|
||||||
|
chunk_size = 4 * 1024 * 1024
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
f.seek(start)
|
||||||
|
remaining = size
|
||||||
|
while remaining > 0:
|
||||||
|
to_read = chunk_size if remaining >= chunk_size else remaining
|
||||||
|
chunk = f.read(to_read)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
buffer.extend(chunk)
|
||||||
|
remaining -= len(chunk)
|
||||||
|
|
||||||
|
if len(buffer) != size:
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"error": "read size mismatch",
|
||||||
|
"expected": size,
|
||||||
|
"actual": len(buffer),
|
||||||
|
},
|
||||||
|
status=500,
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Length": str(size),
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
}
|
||||||
|
|
||||||
|
async with client_session.put(
|
||||||
|
upload_url, data=bytes(buffer), headers=headers
|
||||||
|
) as resp:
|
||||||
|
text = await resp.text()
|
||||||
|
if resp.status < 200 or resp.status >= 300:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": f"upload failed: {resp.status}", "body": text},
|
||||||
|
status=resp.status,
|
||||||
|
)
|
||||||
|
etag = resp.headers.get("ETag") or resp.headers.get("etag") or ""
|
||||||
|
etag = etag.replace('"', "")
|
||||||
|
return web.json_response({"eTag": etag, "bytesSent": size})
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class Status(Enum):
|
|||||||
SUCCESS = "success"
|
SUCCESS = "success"
|
||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
UPLOADING = "uploading"
|
UPLOADING = "uploading"
|
||||||
|
CANCELLED = "cancelled"
|
||||||
|
|
||||||
|
|
||||||
class StreamingPrompt(BaseModel):
|
class StreamingPrompt(BaseModel):
|
||||||
|
|||||||
+1
-1
@@ -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.1"
|
version = "2.3.4"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
|
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
|
||||||
|
|
||||||
|
|||||||
+1049
-194
File diff suppressed because it is too large
Load Diff
@@ -88,7 +88,7 @@ function showNoMachineMessage(machineLoading, machineList) {
|
|||||||
<line x1="6" x2="6.01" y1="18" y2="18"/>
|
<line x1="6" x2="6.01" y1="18" y2="18"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-weight: 500; margin-bottom: 12px;">No machine found</div>
|
<div style="font-weight: 500; margin-bottom: 12px; font-size: 12px;">No machine found</div>
|
||||||
<div style="display: flex; gap: 8px; justify-content: center;">
|
<div style="display: flex; gap: 8px; justify-content: center;">
|
||||||
<button
|
<button
|
||||||
onclick="showAddMachineDialog()"
|
onclick="showAddMachineDialog()"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+957
-3
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user