Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9834dc0b25 | ||
|
|
1939ff4153 | ||
|
|
2b8272bd86 | ||
|
|
ccc9508664 | ||
|
|
b97e523e01 | ||
|
|
ec525e78ad | ||
|
|
4c32248d86 | ||
|
|
5ddbfdf44b | ||
|
|
5fcd43aa54 | ||
|
|
141d30aaa1 | ||
|
|
7e86c20383 | ||
|
|
3adf77617b | ||
|
|
1bc62a5fb4 | ||
|
|
d473a211d0 | ||
|
|
3aa239e58d | ||
|
|
223aa5e70b | ||
|
|
5eef60a4eb | ||
|
|
de750995cb | ||
|
|
0f58fbcebd | ||
|
|
6dc964c425 | ||
|
|
4171c08413 | ||
|
|
4348ab45dc | ||
|
|
df46e3a0e5 | ||
|
|
2772101bbf | ||
|
|
72fee51d32 | ||
|
|
ffe0f98360 | ||
|
|
68377a84bc | ||
|
|
50d4c399e9 | ||
|
|
5a3955dfcb | ||
|
|
03227b52c0 | ||
|
|
8a8fbccfaa | ||
|
|
018d9a7b8d | ||
|
|
774fd566d1 | ||
|
|
b81fcae6fb | ||
|
|
b6b34c9062 | ||
|
|
f73baa091a | ||
|
|
a838cb7ad4 | ||
|
|
2afcade4f2 | ||
|
|
d70333baa6 | ||
|
|
43cfebd97a |
@@ -4,4 +4,15 @@ FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:${VARIANT}
|
|||||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||||
|
|
||||||
|
WORKDIR "/"
|
||||||
|
|
||||||
|
# Install fly
|
||||||
|
RUN curl -L https://fly.io/install.sh | sh
|
||||||
|
|
||||||
|
ENV FLYCTL_INSTALL="/root/.fly"
|
||||||
|
ENV PATH="$FLYCTL_INSTALL/bin:$PATH"
|
||||||
|
|
||||||
|
# RUN echo 'export FLYCTL_INSTALL="/home/node/.fly"' >> ~/.bashrc
|
||||||
|
# RUN echo 'export PATH="$FLYCTL_INSTALL/bin:$PATH"' >> ~/.bashrc
|
||||||
|
|
||||||
RUN npm install -g bun
|
RUN npm install -g bun
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"service": "app",
|
"service": "app",
|
||||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
"postCreateCommand": "cd web && bun install && bun run migrate-local",
|
"postCreateCommand": "cd web && bun install && bun run migrate-local",
|
||||||
|
"remoteUser": "root",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
"stivo.tailwind-fold",
|
"stivo.tailwind-fold",
|
||||||
"streetsidesoftware.code-spell-checker",
|
"streetsidesoftware.code-spell-checker",
|
||||||
"GitHub.copilot",
|
"GitHub.copilot",
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,10 +75,11 @@ Major areas
|
|||||||
3. `bun i`
|
3. `bun i`
|
||||||
4. Start docker
|
4. Start docker
|
||||||
5. `cp .env.example .env.local`
|
5. `cp .env.example .env.local`
|
||||||
6. Repace `JWT_SECRET` with `openssl rand -hex 32`
|
6. Replace `JWT_SECRET` with `openssl rand -hex 32`
|
||||||
7. Get a local clerk dev key for `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`
|
7. Get a local clerk dev key for `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`
|
||||||
8. Keep a terminal live for `bun run db-dev`
|
8. Keep a terminal live for `bun run db-dev`
|
||||||
9. Finally start the next server with `bun dev`
|
9. Execute the local migration to create the initial data `bun run migrate-local`
|
||||||
|
10. Finally start the next server with `bun dev`
|
||||||
|
|
||||||
**Schema Changes**
|
**Schema Changes**
|
||||||
|
|
||||||
|
|||||||
@@ -312,7 +312,8 @@ async def build_logic(item: Item):
|
|||||||
config = {
|
config = {
|
||||||
"name": item.name,
|
"name": item.name,
|
||||||
"deploy_test": os.environ.get("DEPLOY_TEST_FLAG", "False"),
|
"deploy_test": os.environ.get("DEPLOY_TEST_FLAG", "False"),
|
||||||
"gpu": item.gpu
|
"gpu": item.gpu,
|
||||||
|
"civitai_token": os.environ.get("CIVITAI_TOKEN", "")
|
||||||
}
|
}
|
||||||
with open(f"{folder_path}/config.py", "w") as f:
|
with open(f"{folder_path}/config.py", "w") as f:
|
||||||
f.write("config = " + json.dumps(config))
|
f.write("config = " + json.dumps(config))
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ if not deploy_test:
|
|||||||
|
|
||||||
dockerfile_image = (
|
dockerfile_image = (
|
||||||
modal.Image.debian_slim()
|
modal.Image.debian_slim()
|
||||||
|
.env({
|
||||||
|
"CIVITAI_TOKEN": config["civitai_token"],
|
||||||
|
})
|
||||||
.apt_install("git", "wget")
|
.apt_install("git", "wget")
|
||||||
.pip_install(
|
.pip_install(
|
||||||
"git+https://github.com/modal-labs/asgiproxy.git", "httpx", "tqdm"
|
"git+https://github.com/modal-labs/asgiproxy.git", "httpx", "tqdm"
|
||||||
@@ -231,7 +234,8 @@ def run(input: Input):
|
|||||||
async def bar(request_input: RequestInput):
|
async def bar(request_input: RequestInput):
|
||||||
# print(request_input)
|
# print(request_input)
|
||||||
if not deploy_test:
|
if not deploy_test:
|
||||||
return run.remote(request_input.input)
|
run.spawn(request_input.input)
|
||||||
|
return {"status": "success"}
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ with open('models.json') as f:
|
|||||||
models = json.load(f)
|
models = json.load(f)
|
||||||
|
|
||||||
for model in models:
|
for model in models:
|
||||||
|
import os
|
||||||
|
if "civitai.com/api" in model['url'] and not "token=" in model['url']:
|
||||||
|
if "?" in model['url']:
|
||||||
|
model['url'] += "&token=" + os.environ.get('CIVITAI_TOKEN', '')
|
||||||
|
else:
|
||||||
|
model['url'] += "?token=" + os.environ.get('CIVITAI_TOKEN', '')
|
||||||
response = requests.request("POST", f"{root_url}/model/install", json=model, headers=headers)
|
response = requests.request("POST", f"{root_url}/model/install", json=model, headers=headers)
|
||||||
print(response.text)
|
print(response.text)
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ class ComfyUIDeployExternalText:
|
|||||||
CATEGORY = "text"
|
CATEGORY = "text"
|
||||||
|
|
||||||
def run(self, input_id, default_value=None):
|
def run(self, input_id, default_value=None):
|
||||||
if not input_id or len(input_id.strip()) == 0:
|
|
||||||
return [default_value]
|
return [default_value]
|
||||||
return [input_id]
|
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalText": ComfyUIDeployExternalText}
|
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalText": ComfyUIDeployExternalText}
|
||||||
|
|||||||
+228
-14
@@ -22,10 +22,14 @@ from logging.handlers import RotatingFileHandler
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
import threading
|
import threading
|
||||||
|
import hashlib
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
api = None
|
api = None
|
||||||
api_task = None
|
api_task = None
|
||||||
prompt_metadata = {}
|
prompt_metadata = {}
|
||||||
|
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'
|
||||||
|
|
||||||
def post_prompt(json_data):
|
def post_prompt(json_data):
|
||||||
prompt_server = server.PromptServer.instance
|
prompt_server = server.PromptServer.instance
|
||||||
@@ -97,6 +101,7 @@ async def comfy_deploy_run(request):
|
|||||||
prompt_metadata[prompt_id] = {
|
prompt_metadata[prompt_id] = {
|
||||||
'status_endpoint': data.get('status_endpoint'),
|
'status_endpoint': data.get('status_endpoint'),
|
||||||
'file_upload_endpoint': data.get('file_upload_endpoint'),
|
'file_upload_endpoint': data.get('file_upload_endpoint'),
|
||||||
|
'workflow_api': workflow_api
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -113,6 +118,8 @@ async def comfy_deploy_run(request):
|
|||||||
"stack_trace": stack_trace
|
"stack_trace": stack_trace
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
# When there are critical errors, the prompt is actually not run
|
||||||
|
await update_run(prompt_id, Status.FAILED)
|
||||||
return web.Response(status=500, reason=f"{error_type}: {e}, {stack_trace_short}")
|
return web.Response(status=500, reason=f"{error_type}: {e}, {stack_trace_short}")
|
||||||
|
|
||||||
status = 200
|
status = 200
|
||||||
@@ -141,6 +148,138 @@ async def comfy_deploy_run(request):
|
|||||||
|
|
||||||
sockets = dict()
|
sockets = dict()
|
||||||
|
|
||||||
|
def get_comfyui_path_from_file_path(file_path):
|
||||||
|
file_path_parts = file_path.split("\\")
|
||||||
|
|
||||||
|
if file_path_parts[0] == "input":
|
||||||
|
print("matching input")
|
||||||
|
file_path = os.path.join(folder_paths.get_directory_by_type("input"), *file_path_parts[1:])
|
||||||
|
elif file_path_parts[0] == "models":
|
||||||
|
print("matching models")
|
||||||
|
file_path = folder_paths.get_full_path(file_path_parts[1], os.path.join(*file_path_parts[2:]))
|
||||||
|
|
||||||
|
print(file_path)
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
# Form ComfyUI Manager
|
||||||
|
def compute_sha256_checksum(filepath):
|
||||||
|
filepath = get_comfyui_path_from_file_path(filepath)
|
||||||
|
"""Compute the SHA256 checksum of a file, in chunks"""
|
||||||
|
sha256 = hashlib.sha256()
|
||||||
|
with open(filepath, 'rb') as f:
|
||||||
|
for chunk in iter(lambda: f.read(4096), b''):
|
||||||
|
sha256.update(chunk)
|
||||||
|
return sha256.hexdigest()
|
||||||
|
|
||||||
|
# This is start uploading the files to Comfy Deploy
|
||||||
|
@server.PromptServer.instance.routes.post('/comfyui-deploy/upload-file')
|
||||||
|
async def upload_file(request):
|
||||||
|
data = await request.json()
|
||||||
|
|
||||||
|
file_path = data.get("file_path")
|
||||||
|
|
||||||
|
print("Original file path", file_path)
|
||||||
|
|
||||||
|
file_path = get_comfyui_path_from_file_path(file_path)
|
||||||
|
|
||||||
|
# return web.json_response({
|
||||||
|
# "error": f"File not uploaded"
|
||||||
|
# }, status=500)
|
||||||
|
|
||||||
|
token = data.get("token")
|
||||||
|
get_url = data.get("url")
|
||||||
|
|
||||||
|
try:
|
||||||
|
base = folder_paths.base_path
|
||||||
|
file_path = os.path.join(base, file_path)
|
||||||
|
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
file_size = os.path.getsize(file_path)
|
||||||
|
file_extension = os.path.splitext(file_path)[1]
|
||||||
|
|
||||||
|
if file_extension in ['.jpg', '.jpeg']:
|
||||||
|
file_type = 'image/jpeg'
|
||||||
|
elif file_extension == '.png':
|
||||||
|
file_type = 'image/png'
|
||||||
|
elif file_extension == '.webp':
|
||||||
|
file_type = 'image/webp'
|
||||||
|
else:
|
||||||
|
file_type = 'application/octet-stream' # Default to binary file type if unknown
|
||||||
|
else:
|
||||||
|
return web.json_response({
|
||||||
|
"error": f"File not found: {file_path}"
|
||||||
|
}, status=404)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({
|
||||||
|
"error": str(e)
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
|
if get_url:
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
headers = {'Authorization': f'Bearer {token}'}
|
||||||
|
params = {'file_size': file_size, 'type': file_type}
|
||||||
|
async with session.get(get_url, params=params, headers=headers) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
content = await response.json()
|
||||||
|
upload_url = content["upload_url"]
|
||||||
|
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
headers = {
|
||||||
|
"Content-Type": file_type,
|
||||||
|
"x-amz-acl": "public-read",
|
||||||
|
"Content-Length": str(file_size)
|
||||||
|
}
|
||||||
|
async with session.put(upload_url, data=f, headers=headers) as upload_response:
|
||||||
|
if upload_response.status == 200:
|
||||||
|
return web.json_response({
|
||||||
|
"message": "File uploaded successfully",
|
||||||
|
"download_url": content["download_url"]
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return web.json_response({
|
||||||
|
"error": f"Failed to upload file to {upload_url}. Status code: {upload_response.status}"
|
||||||
|
}, status=upload_response.status)
|
||||||
|
else:
|
||||||
|
return web.json_response({
|
||||||
|
"error": f"Failed to fetch data from {get_url}. Status code: {response.status}"
|
||||||
|
}, status=response.status)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({
|
||||||
|
"error": f"An error occurred while fetching data from {get_url}: {str(e)}"
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
"error": f"File not uploaded"
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@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 file_path is None:
|
||||||
|
return web.json_response({
|
||||||
|
"error": "file_path is required"
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
base = folder_paths.base_path
|
||||||
|
file_path = os.path.join(base, file_path)
|
||||||
|
# print("file_path", file_path)
|
||||||
|
file_hash = compute_sha256_checksum(
|
||||||
|
file_path
|
||||||
|
)
|
||||||
|
return web.json_response({
|
||||||
|
"file_hash": file_hash
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({
|
||||||
|
"error": str(e)
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.get('/comfyui-deploy/ws')
|
@server.PromptServer.instance.routes.get('/comfyui-deploy/ws')
|
||||||
async def websocket_handler(request):
|
async def websocket_handler(request):
|
||||||
ws = web.WebSocketResponse()
|
ws = web.WebSocketResponse()
|
||||||
@@ -157,6 +296,8 @@ async def websocket_handler(request):
|
|||||||
try:
|
try:
|
||||||
# Send initial state to the new client
|
# Send initial state to the new client
|
||||||
await send("status", { 'sid': sid }, sid)
|
await send("status", { 'sid': sid }, sid)
|
||||||
|
|
||||||
|
if cd_enable_log:
|
||||||
await send_first_time_log(sid)
|
await send_first_time_log(sid)
|
||||||
|
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
@@ -217,6 +358,17 @@ async def send_json_override(self, event, data, sid=None):
|
|||||||
if not have_pending_upload(prompt_id):
|
if not have_pending_upload(prompt_id):
|
||||||
update_run(prompt_id, Status.SUCCESS)
|
update_run(prompt_id, Status.SUCCESS)
|
||||||
|
|
||||||
|
if event == 'executing' and data.get('node') is not None:
|
||||||
|
node = data.get('node')
|
||||||
|
|
||||||
|
if 'prompt_id' in prompt_metadata:
|
||||||
|
if 'last_updated_node' in prompt_metadata[prompt_id] and prompt_metadata[prompt_id]['last_updated_node'] == node:
|
||||||
|
return
|
||||||
|
prompt_metadata[prompt_id]['last_updated_node'] = node
|
||||||
|
class_type = prompt_metadata[prompt_id]['workflow_api'][node]['class_type']
|
||||||
|
print("updating run live status", class_type)
|
||||||
|
await update_run_live_status(prompt_id, "Executing " + class_type)
|
||||||
|
|
||||||
if event == 'execution_error':
|
if event == 'execution_error':
|
||||||
# Careful this might not be fully awaited.
|
# Careful this might not be fully awaited.
|
||||||
await update_run_with_output(prompt_id, data)
|
await update_run_with_output(prompt_id, data)
|
||||||
@@ -236,7 +388,26 @@ class Status(Enum):
|
|||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
UPLOADING = "uploading"
|
UPLOADING = "uploading"
|
||||||
|
|
||||||
|
# Global variable to keep track of the last read line number
|
||||||
|
last_read_line_number = 0
|
||||||
|
|
||||||
|
async def update_run_live_status(prompt_id, live_status):
|
||||||
|
if prompt_id not in prompt_metadata:
|
||||||
|
return
|
||||||
|
|
||||||
|
status_endpoint = prompt_metadata[prompt_id]['status_endpoint']
|
||||||
|
body = {
|
||||||
|
"run_id": prompt_id,
|
||||||
|
"live_status": live_status,
|
||||||
|
}
|
||||||
|
# requests.post(status_endpoint, json=body)
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
await session.post(status_endpoint, json=body)
|
||||||
|
|
||||||
|
|
||||||
def update_run(prompt_id, status: Status):
|
def update_run(prompt_id, status: Status):
|
||||||
|
global last_read_line_number
|
||||||
|
|
||||||
if prompt_id not in prompt_metadata:
|
if prompt_id not in prompt_metadata:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -251,15 +422,49 @@ def update_run(prompt_id, status: Status):
|
|||||||
"run_id": prompt_id,
|
"run_id": prompt_id,
|
||||||
"status": status.value,
|
"status": status.value,
|
||||||
}
|
}
|
||||||
prompt_metadata[prompt_id]['status'] = status
|
|
||||||
print(f"Status: {status.value}")
|
print(f"Status: {status.value}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
requests.post(status_endpoint, json=body)
|
requests.post(status_endpoint, json=body)
|
||||||
|
|
||||||
|
if cd_enable_run_log and (status == Status.SUCCESS or status == Status.FAILED):
|
||||||
|
try:
|
||||||
|
with open(comfyui_file_path, 'r') as log_file:
|
||||||
|
# log_data = log_file.read()
|
||||||
|
# Move to the last read line
|
||||||
|
all_log_data = log_file.read() # Read all log data
|
||||||
|
print("All log data before skipping:", all_log_data) # Log all data before skipping
|
||||||
|
log_file.seek(0) # Reset file pointer to the beginning
|
||||||
|
|
||||||
|
for _ in range(last_read_line_number):
|
||||||
|
next(log_file)
|
||||||
|
log_data = log_file.read()
|
||||||
|
# Update the last read line number
|
||||||
|
last_read_line_number += log_data.count('\n')
|
||||||
|
print("last_read_line_number", last_read_line_number)
|
||||||
|
print("log_data", log_data)
|
||||||
|
print("log_data.count(n)", log_data.count('\n'))
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"run_id": prompt_id,
|
||||||
|
"log_data": [
|
||||||
|
{
|
||||||
|
"logs": log_data,
|
||||||
|
# "timestamp": time.time(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
requests.post(status_endpoint, json=body)
|
||||||
|
except Exception as log_error:
|
||||||
|
print(f"Error reading log file: {log_error}")
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_type = type(e).__name__
|
error_type = type(e).__name__
|
||||||
stack_trace = traceback.format_exc().strip()
|
stack_trace = traceback.format_exc().strip()
|
||||||
print(f"Error occurred while updating run: {e} {stack_trace}")
|
print(f"Error occurred while updating run: {e} {stack_trace}")
|
||||||
|
finally:
|
||||||
|
prompt_metadata[prompt_id]['status'] = status
|
||||||
|
|
||||||
|
|
||||||
async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output"):
|
async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output"):
|
||||||
@@ -310,7 +515,9 @@ async def upload_file(prompt_id, filename, subfolder=None, content_type="image/p
|
|||||||
"Content-Length": str(len(data)),
|
"Content-Length": str(len(data)),
|
||||||
}
|
}
|
||||||
response = requests.put(ok.get("url"), headers=headers, data=data)
|
response = requests.put(ok.get("url"), headers=headers, data=data)
|
||||||
print("upload file response", response.status_code)
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.put(ok.get("url"), headers=headers, data=data) as response:
|
||||||
|
print("upload file response", response.status)
|
||||||
|
|
||||||
def have_pending_upload(prompt_id):
|
def have_pending_upload(prompt_id):
|
||||||
if 'prompt_id' in prompt_metadata and 'uploading_nodes' in prompt_metadata[prompt_id] and len(prompt_metadata[prompt_id]['uploading_nodes']) > 0:
|
if 'prompt_id' in prompt_metadata and 'uploading_nodes' in prompt_metadata[prompt_id] and len(prompt_metadata[prompt_id]['uploading_nodes']) > 0:
|
||||||
@@ -387,20 +594,25 @@ async def update_file_status(prompt_id, data, uploading, have_error=False, node_
|
|||||||
"prompt_id": prompt_id,
|
"prompt_id": prompt_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async def handle_upload(prompt_id, data, key, content_type_key, default_content_type):
|
||||||
|
items = data.get(key, [])
|
||||||
|
for item in items:
|
||||||
|
await upload_file(
|
||||||
|
prompt_id,
|
||||||
|
item.get("filename"),
|
||||||
|
subfolder=item.get("subfolder"),
|
||||||
|
type=item.get("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, data, node_id=None, have_upload=True):
|
async def upload_in_background(prompt_id, data, node_id=None, have_upload=True):
|
||||||
try:
|
try:
|
||||||
images = data.get('images', [])
|
await handle_upload(prompt_id, data, 'images', "content_type", "image/png")
|
||||||
for image in images:
|
await handle_upload(prompt_id, data, 'files', "content_type", "image/png")
|
||||||
await upload_file(prompt_id, image.get("filename"), subfolder=image.get("subfolder"), type=image.get("type"), content_type=image.get("content_type", "image/png"))
|
# This will also be mp4
|
||||||
|
await handle_upload(prompt_id, data, 'gifs', "format", "image/gif")
|
||||||
files = data.get('files', [])
|
|
||||||
for file in files:
|
|
||||||
await upload_file(prompt_id, file.get("filename"), subfolder=file.get("subfolder"), type=file.get("type"), content_type=file.get("content_type", "image/png"))
|
|
||||||
|
|
||||||
gifs = data.get('gifs', [])
|
|
||||||
for gif in gifs:
|
|
||||||
await upload_file(prompt_id, gif.get("filename"), subfolder=gif.get("subfolder"), type=gif.get("type"), content_type=gif.get("format", "image/gif"))
|
|
||||||
|
|
||||||
if have_upload:
|
if have_upload:
|
||||||
await update_file_status(prompt_id, data, False, node_id=node_id)
|
await update_file_status(prompt_id, data, False, node_id=node_id)
|
||||||
@@ -441,6 +653,7 @@ prompt_server.send_json = send_json_override.__get__(prompt_server, server.Promp
|
|||||||
root_path = os.path.dirname(os.path.abspath(__file__))
|
root_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
two_dirs_up = os.path.dirname(os.path.dirname(root_path))
|
two_dirs_up = os.path.dirname(os.path.dirname(root_path))
|
||||||
log_file_path = os.path.join(two_dirs_up, 'comfy-deploy.log')
|
log_file_path = os.path.join(two_dirs_up, 'comfy-deploy.log')
|
||||||
|
comfyui_file_path = os.path.join(two_dirs_up, 'comfyui.log')
|
||||||
|
|
||||||
last_read_line = 0
|
last_read_line = 0
|
||||||
|
|
||||||
@@ -480,4 +693,5 @@ def run_in_new_thread(coroutine):
|
|||||||
t.start()
|
t.start()
|
||||||
asyncio.run_coroutine_threadsafe(coroutine, new_loop)
|
asyncio.run_coroutine_threadsafe(coroutine, new_loop)
|
||||||
|
|
||||||
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
|
if cd_enable_log:
|
||||||
|
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
|
||||||
|
|||||||
+18
-7
@@ -7,12 +7,19 @@ import threading
|
|||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5)
|
# Running with export CD_ENABLE_LOG=true; python main.py
|
||||||
|
|
||||||
original_stdout = sys.stdout
|
# Check for 'cd-enable-log' flag in input arguments
|
||||||
original_stderr = sys.stderr
|
# cd_enable_log = '--cd-enable-log' in sys.argv
|
||||||
|
cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true'
|
||||||
|
|
||||||
class StreamToLogger():
|
def setup():
|
||||||
|
handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5)
|
||||||
|
|
||||||
|
original_stdout = sys.stdout
|
||||||
|
original_stderr = sys.stderr
|
||||||
|
|
||||||
|
class StreamToLogger():
|
||||||
def __init__(self, log_level):
|
def __init__(self, log_level):
|
||||||
self.log_level = log_level
|
self.log_level = log_level
|
||||||
|
|
||||||
@@ -43,9 +50,13 @@ class StreamToLogger():
|
|||||||
elif (self.log_level == logging.ERROR):
|
elif (self.log_level == logging.ERROR):
|
||||||
original_stderr.flush()
|
original_stderr.flush()
|
||||||
|
|
||||||
# Redirect stdout and stderr to the logger
|
# Redirect stdout and stderr to the logger
|
||||||
sys.stdout = StreamToLogger(logging.INFO)
|
sys.stdout = StreamToLogger(logging.INFO)
|
||||||
sys.stderr = StreamToLogger(logging.ERROR)
|
sys.stderr = StreamToLogger(logging.ERROR)
|
||||||
|
|
||||||
|
if cd_enable_log:
|
||||||
|
print("** Comfy Deploy logging enabled")
|
||||||
|
setup()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get the absolute path of the script's directory
|
# Get the absolute path of the script's directory
|
||||||
|
|||||||
+215
-24
@@ -1,6 +1,7 @@
|
|||||||
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]";
|
||||||
|
|
||||||
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
|
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
|
||||||
/** @type {ComfyExtension} */
|
/** @type {ComfyExtension} */
|
||||||
@@ -15,9 +16,7 @@ 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");
|
||||||
if (!workflow_version_id) {
|
|
||||||
console.error("No workflow_version_id provided in query parameters.");
|
|
||||||
} else {
|
|
||||||
const data = getData();
|
const data = getData();
|
||||||
let endpoint = data.endpoint;
|
let endpoint = data.endpoint;
|
||||||
let apiKey = data.apiKey;
|
let apiKey = data.apiKey;
|
||||||
@@ -36,6 +35,9 @@ const ext = {
|
|||||||
localStorage.setItem("comfy_deploy_env", "cloud");
|
localStorage.setItem("comfy_deploy_env", "cloud");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!workflow_version_id) {
|
||||||
|
console.error("No workflow_version_id provided in query parameters.");
|
||||||
|
} else {
|
||||||
loadingDialog.showLoading(
|
loadingDialog.showLoading(
|
||||||
"Loading workflow from " + org_display,
|
"Loading workflow from " + org_display,
|
||||||
"Please wait...",
|
"Please wait...",
|
||||||
@@ -49,11 +51,22 @@ const ext = {
|
|||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const { workflow, error } = data;
|
const { workflow, workflow_id, error } = data;
|
||||||
if (error) {
|
if (error) {
|
||||||
infoDialog.showMessage("Unable to load this workflow", error);
|
infoDialog.showMessage("Unable to load this workflow", error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adding a delay to wait for the intial graph to load
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
workflow?.nodes.forEach((x) => {
|
||||||
|
if (x?.type === "ComfyDeploy") {
|
||||||
|
x.widgets_values[1] = workflow_id;
|
||||||
|
// x.widgets_values[2] = workflow_version.version;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/** @type {LGraph} */
|
/** @type {LGraph} */
|
||||||
app.loadGraphData(workflow);
|
app.loadGraphData(workflow);
|
||||||
})
|
})
|
||||||
@@ -166,6 +179,92 @@ function showError(title, message) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createDynamicUIHtml(data) {
|
||||||
|
console.log(data);
|
||||||
|
let html =
|
||||||
|
'<div style="max-width: 1024px; margin: 14px auto; display: flex; flex-direction: column; gap: 24px;">';
|
||||||
|
const bgcolor = "var(--comfy-input-bg)";
|
||||||
|
const evenBg = "var(--border-color)";
|
||||||
|
const textColor = "var(--input-text)";
|
||||||
|
|
||||||
|
// Custom Nodes
|
||||||
|
html += `<div style="background-color: ${bgcolor}; padding: 24px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">`;
|
||||||
|
html +=
|
||||||
|
'<h2 style="margin-top: 0px; font-size: 24px; font-weight: bold; margin-bottom: 16px;">Custom Nodes</h2>';
|
||||||
|
|
||||||
|
if (data.missing_nodes?.length > 0) {
|
||||||
|
html += `
|
||||||
|
<div style="border-bottom: 1px solid #e2e8f0; padding: 4px 12px; background-color: ${evenBg}">
|
||||||
|
<h3 style="font-size: 14px; font-weight: semibold; margin-bottom: 8px;">Missing Nodes</h3>
|
||||||
|
<p style="font-size: 12px;">These nodes are not found with any matching custom_nodes in the ComfyUI Manager Database</p>
|
||||||
|
${data.missing_nodes
|
||||||
|
.map((node) => {
|
||||||
|
return `<p style="font-size: 14px; color: #d69e2e;">${node}</p>`;
|
||||||
|
})
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(data.custom_nodes).forEach((node) => {
|
||||||
|
html += `
|
||||||
|
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 16px;">
|
||||||
|
<a href="${
|
||||||
|
node.url
|
||||||
|
}" target="_blank" style="font-size: 18px; font-weight: semibold; color: white; text-decoration: none;">${
|
||||||
|
node.name
|
||||||
|
}</a>
|
||||||
|
<p style="font-size: 14px; color: #4b5563;">${node.hash}</p>
|
||||||
|
${
|
||||||
|
node.warning
|
||||||
|
? `<p style="font-size: 14px; color: #d69e2e;">${node.warning}</p>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
html += "</div>";
|
||||||
|
|
||||||
|
// Models
|
||||||
|
html += `<div style="background-color: ${bgcolor}; padding: 24px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">`;
|
||||||
|
html +=
|
||||||
|
'<h2 style="margin-top: 0px; font-size: 24px; font-weight: bold; margin-bottom: 16px;">Models</h2>';
|
||||||
|
|
||||||
|
Object.entries(data.models).forEach(([section, items]) => {
|
||||||
|
html += `
|
||||||
|
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;">
|
||||||
|
<h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${
|
||||||
|
section.charAt(0).toUpperCase() + section.slice(1)
|
||||||
|
}</h3>`;
|
||||||
|
items.forEach((item) => {
|
||||||
|
html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`;
|
||||||
|
});
|
||||||
|
html += "</div>";
|
||||||
|
});
|
||||||
|
html += "</div>";
|
||||||
|
|
||||||
|
// Models
|
||||||
|
html += `<div style="background-color: ${bgcolor}; padding: 24px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">`;
|
||||||
|
html +=
|
||||||
|
'<h2 style="margin-top: 0px; font-size: 24px; font-weight: bold; margin-bottom: 16px;">Files</h2>';
|
||||||
|
|
||||||
|
Object.entries(data.files).forEach(([section, items]) => {
|
||||||
|
html += `
|
||||||
|
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;">
|
||||||
|
<h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${
|
||||||
|
section.charAt(0).toUpperCase() + section.slice(1)
|
||||||
|
}</h3>`;
|
||||||
|
items.forEach((item) => {
|
||||||
|
html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`;
|
||||||
|
});
|
||||||
|
html += "</div>";
|
||||||
|
});
|
||||||
|
html += "</div>";
|
||||||
|
|
||||||
|
html += "</div>";
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function addButton() {
|
function addButton() {
|
||||||
const menu = document.querySelector(".comfy-menu");
|
const menu = document.querySelector(".comfy-menu");
|
||||||
|
|
||||||
@@ -177,8 +276,29 @@ function addButton() {
|
|||||||
/** @type {LGraph} */
|
/** @type {LGraph} */
|
||||||
const graph = app.graph;
|
const graph = app.graph;
|
||||||
|
|
||||||
|
let { endpoint, apiKey, displayName } = getData();
|
||||||
|
|
||||||
|
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
|
||||||
|
configDialog.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await confirmDialog.confirm(
|
||||||
|
"Confirm deployment -> " + displayName,
|
||||||
|
`A new version will be deployed, are you conform? <br><br><input id="include-deps" type="checkbox" checked>Include dependence</input>`,
|
||||||
|
);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
const includeDeps = document.getElementById("include-deps").checked;
|
||||||
|
|
||||||
|
if (endpoint.endsWith("/")) {
|
||||||
|
endpoint = endpoint.slice(0, -1);
|
||||||
|
}
|
||||||
|
loadingDialog.showLoading("Generating snapshot", "Please wait...");
|
||||||
|
|
||||||
const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
|
const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
|
||||||
// console.log(snapshot);
|
// console.log(snapshot);
|
||||||
|
loadingDialog.close();
|
||||||
|
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
showError(
|
showError(
|
||||||
@@ -212,32 +332,94 @@ function addButton() {
|
|||||||
|
|
||||||
const deployMetaNode = deployMeta[0];
|
const deployMetaNode = deployMeta[0];
|
||||||
|
|
||||||
// console.log(deployMetaNode);
|
|
||||||
|
|
||||||
const workflow_name = deployMetaNode.widgets[0].value;
|
const workflow_name = deployMetaNode.widgets[0].value;
|
||||||
const workflow_id = deployMetaNode.widgets[1].value;
|
const workflow_id = deployMetaNode.widgets[1].value;
|
||||||
|
|
||||||
console.log(workflow_name, workflow_id);
|
|
||||||
|
|
||||||
const prompt = await app.graphToPrompt();
|
const prompt = await app.graphToPrompt();
|
||||||
console.log(graph);
|
let deps = undefined;
|
||||||
console.log(prompt);
|
|
||||||
|
|
||||||
// const endpoint = localStorage.getItem("endpoint") ?? "";
|
if (includeDeps) {
|
||||||
// const apiKey = localStorage.getItem("apiKey");
|
loadingDialog.showLoading("Fetching existing version", "Please wait...");
|
||||||
|
|
||||||
const { endpoint, apiKey, displayName } = getData();
|
const existing_workflow = await fetch(
|
||||||
|
endpoint + "/api/workflow/" + workflow_id,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + apiKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((x) => x.json())
|
||||||
|
.catch(() => {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
|
loadingDialog.close();
|
||||||
configDialog.show();
|
|
||||||
return;
|
loadingDialog.showLoading(
|
||||||
|
"Generating dependency graph",
|
||||||
|
"Please wait...",
|
||||||
|
);
|
||||||
|
deps = await generateDependencyGraph({
|
||||||
|
workflow_api: prompt.output,
|
||||||
|
snapshot: snapshot,
|
||||||
|
computeFileHash: async (file) => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
loadingDialog.close();
|
||||||
|
|
||||||
|
const depsOk = await confirmDialog.confirm(
|
||||||
|
"Check dependencies",
|
||||||
|
// JSON.stringify(deps, null, 2),
|
||||||
|
createDynamicUIHtml(deps),
|
||||||
|
);
|
||||||
|
if (!depsOk) return;
|
||||||
|
|
||||||
|
console.log(deps);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ok = await confirmDialog.confirm(
|
loadingDialog.showLoading("Deploying...");
|
||||||
"Confirm deployment -> " + displayName,
|
|
||||||
"A new version will be deployed, are you conform?",
|
|
||||||
);
|
|
||||||
if (!ok) return;
|
|
||||||
|
|
||||||
title.innerText = "Deploying...";
|
title.innerText = "Deploying...";
|
||||||
title.style.color = "orange";
|
title.style.color = "orange";
|
||||||
@@ -249,6 +431,8 @@ function addButton() {
|
|||||||
endpoint = endpoint.slice(0, -1);
|
endpoint = endpoint.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log(prompt.workflow);
|
||||||
|
|
||||||
const apiRoute = endpoint + "/api/workflow";
|
const apiRoute = endpoint + "/api/workflow";
|
||||||
// const userId = apiKey
|
// const userId = apiKey
|
||||||
try {
|
try {
|
||||||
@@ -258,6 +442,7 @@ function addButton() {
|
|||||||
workflow: prompt.workflow,
|
workflow: prompt.workflow,
|
||||||
workflow_api: prompt.output,
|
workflow_api: prompt.output,
|
||||||
snapshot: snapshot,
|
snapshot: snapshot,
|
||||||
|
dependencies: deps,
|
||||||
};
|
};
|
||||||
console.log(body);
|
console.log(body);
|
||||||
let data = await fetch(apiRoute, {
|
let data = await fetch(apiRoute, {
|
||||||
@@ -277,6 +462,8 @@ function addButton() {
|
|||||||
data = await data.json();
|
data = await data.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadingDialog.close();
|
||||||
|
|
||||||
title.textContent = "Done";
|
title.textContent = "Done";
|
||||||
title.style.color = "green";
|
title.style.color = "green";
|
||||||
|
|
||||||
@@ -293,6 +480,7 @@ function addButton() {
|
|||||||
title.style.color = "white";
|
title.style.color = "white";
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
loadingDialog.close();
|
||||||
app.ui.dialog.show(e);
|
app.ui.dialog.show(e);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
title.textContent = "Error";
|
title.textContent = "Error";
|
||||||
@@ -369,7 +557,7 @@ export class InfoDialog extends ComfyDialog {
|
|||||||
|
|
||||||
showMessage(title, message) {
|
showMessage(title, message) {
|
||||||
this.show(`
|
this.show(`
|
||||||
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
|
<div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
|
||||||
<h3 style="margin: 0px;">${title}</h3>
|
<h3 style="margin: 0px;">${title}</h3>
|
||||||
<label>
|
<label>
|
||||||
${message}
|
${message}
|
||||||
@@ -425,7 +613,10 @@ export class LoadingDialog extends ComfyDialog {
|
|||||||
showLoading(title, message) {
|
showLoading(title, message) {
|
||||||
this.show(`
|
this.show(`
|
||||||
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
|
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
|
||||||
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 4px;">${title} ${this.loadingIcon}</h3>
|
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${
|
||||||
|
this.loadingIcon
|
||||||
|
}</h3>
|
||||||
|
${message ? `<label>${message}</label>` : ""}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -544,7 +735,7 @@ export class ConfirmDialog extends InfoDialog {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.callback = resolve;
|
this.callback = resolve;
|
||||||
this.show(`
|
this.show(`
|
||||||
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
|
<div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
|
||||||
<h3 style="margin: 0px;">${title}</h3>
|
<h3 style="margin: 0px;">${title}</h3>
|
||||||
<label>
|
<label>
|
||||||
${message}
|
${message}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
const pathName = usePathname();
|
||||||
|
return <LoadingPageWrapper className="h-full" tag={pathName.toLowerCase()} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <Examples />;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type exampleWorkflow = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
previewURL: string;
|
||||||
|
image: {
|
||||||
|
src: string,
|
||||||
|
alt: string,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const exampleWorkflows: exampleWorkflow[] = [
|
||||||
|
{
|
||||||
|
title: "Txt2Img SDXL",
|
||||||
|
description: "The basic workflow, type a prompt and generate images based on that.",
|
||||||
|
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-txt2img-sdxl',
|
||||||
|
image: {
|
||||||
|
src: '/example-workflows/txt2img.webp',
|
||||||
|
alt: 'IPAdapter workflow',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Txt2Img LCM SDXL",
|
||||||
|
description: "Images in a couple of seconds, increase the speed of each generation using LCM Lora.",
|
||||||
|
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-lcm-sdxl',
|
||||||
|
image: {
|
||||||
|
src: '/example-workflows/txt2img-lcm.webp',
|
||||||
|
alt: 'txt2img LCM SDXL',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "IPAdapter SDXL",
|
||||||
|
description: "Load images and use them as reference for new generations.",
|
||||||
|
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-ip-adapter-sdxl',
|
||||||
|
image: {
|
||||||
|
src: '/example-workflows/ipadapter.webp',
|
||||||
|
alt: 'IPAdapter workflow',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Upscale and Add Detail SDXL",
|
||||||
|
description: "Upscale and Add Details to your creations",
|
||||||
|
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-upscale-and-add-detail-sdxl',
|
||||||
|
image: {
|
||||||
|
src: '/example-workflows/upscale.webp',
|
||||||
|
alt: 'Upscale and Add Detail SDXL',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
async function Examples() {
|
||||||
|
return (
|
||||||
|
<div className="w-full py-4">
|
||||||
|
<section className="mx-auto flex max-w-[980px] flex-col items-center gap-2 py-8 md:py-12 md:pb-8 lg:py-24 lg:pb-20">
|
||||||
|
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl text-center">
|
||||||
|
Check out some examples
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-[560px] text-center text-lg text-muted-foreground">Text to Image, Image to Image, IPAdapter, and more. Here are some examples that you can use to deploy your workflow.</p>
|
||||||
|
</section>
|
||||||
|
<section className="flex justify-center flex-wrap gap-4">
|
||||||
|
{exampleWorkflows.map(workflow => {
|
||||||
|
return <Card className="w-[350px]">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{workflow.title}</CardTitle>
|
||||||
|
<CardDescription>{workflow.description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Image src={workflow.image.src} alt={workflow.image.alt} width={350} height={230} />
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-end gap-2">
|
||||||
|
<Button asChild>
|
||||||
|
<Link href={workflow.previewURL}>View Workflow</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>;
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ export function Navbar() {
|
|||||||
const { organization } = useOrganization();
|
const { organization } = useOrganization();
|
||||||
const _isDesktop = useMediaQuery("(min-width: 1024px)");
|
const _isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||||
const [isDesktop, setIsDesktop] = useState(true);
|
const [isDesktop, setIsDesktop] = useState(true);
|
||||||
|
const [isSheetOpen, setSheetOpen] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsDesktop(_isDesktop);
|
setIsDesktop(_isDesktop);
|
||||||
}, [_isDesktop]);
|
}, [_isDesktop]);
|
||||||
@@ -36,7 +37,7 @@ export function Navbar() {
|
|||||||
<>
|
<>
|
||||||
<div className="flex flex-row items-center gap-4">
|
<div className="flex flex-row items-center gap-4">
|
||||||
{!isDesktop && (
|
{!isDesktop && (
|
||||||
<Sheet>
|
<Sheet open={isSheetOpen} onOpenChange={(open) => setSheetOpen(open)}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<button className="flex items-center justify-center w-8 h-8 p-2">
|
<button className="flex items-center justify-center w-8 h-8 p-2">
|
||||||
<Menu />
|
<Menu />
|
||||||
@@ -47,7 +48,10 @@ export function Navbar() {
|
|||||||
<SheetTitle className="text-start">Comfy Deploy</SheetTitle>
|
<SheetTitle className="text-start">Comfy Deploy</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="grid h-full grid-rows-[1fr_auto]">
|
<div className="grid h-full grid-rows-[1fr_auto]">
|
||||||
<NavbarMenu className=" h-full" />
|
<NavbarMenu
|
||||||
|
className=" h-full"
|
||||||
|
closeSheet={() => setSheetOpen(false)}
|
||||||
|
/>
|
||||||
{/* <OrganizationSwitcher
|
{/* <OrganizationSwitcher
|
||||||
appearance={{
|
appearance={{
|
||||||
elements: {
|
elements: {
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useMediaQuery } from "usehooks-ts";
|
import { useMediaQuery } from "usehooks-ts";
|
||||||
|
|
||||||
export function NavbarMenu({ className }: { className?: string }) {
|
export function NavbarMenu({
|
||||||
|
className,
|
||||||
|
closeSheet,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
closeSheet?: () => void;
|
||||||
|
}) {
|
||||||
const _isDesktop = useMediaQuery("(min-width: 1024px)");
|
const _isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||||
const [isDesktop, setIsDesktop] = useState(true);
|
const [isDesktop, setIsDesktop] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,6 +40,10 @@ export function NavbarMenu({ className }: { className?: string }) {
|
|||||||
name: "API Keys",
|
name: "API Keys",
|
||||||
path: "/api-keys",
|
path: "/api-keys",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Examples",
|
||||||
|
path: "/examples"
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,9 +52,9 @@ export function NavbarMenu({ className }: { className?: string }) {
|
|||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultValue={pathname}
|
defaultValue={pathname}
|
||||||
className="w-[300px] flex pointer-events-auto"
|
className="w-fit flex pointer-events-auto"
|
||||||
>
|
>
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<TabsList className="w-full">
|
||||||
{pages.map((page) => (
|
{pages.map((page) => (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={page.name}
|
key={page.name}
|
||||||
@@ -68,6 +78,9 @@ export function NavbarMenu({ className }: { className?: string }) {
|
|||||||
<Link
|
<Link
|
||||||
key={page.name}
|
key={page.name}
|
||||||
href={page.path}
|
href={page.path}
|
||||||
|
onClick={() => {
|
||||||
|
if (!!closeSheet) closeSheet();
|
||||||
|
}}
|
||||||
className="p-2 hover:bg-gray-100/20 hover:underline"
|
className="p-2 hover:bg-gray-100/20 hover:underline"
|
||||||
>
|
>
|
||||||
{page.name}
|
{page.name}
|
||||||
|
|||||||
@@ -5,6 +5,20 @@ export async function OutputRender(props: {
|
|||||||
run_id: string;
|
run_id: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
}) {
|
}) {
|
||||||
|
if (props.filename.endsWith(".mp4") || props.filename.endsWith(".webm")) {
|
||||||
|
const url = await getFileDownloadUrl(
|
||||||
|
`outputs/runs/${props.run_id}/${props.filename}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<video controls autoPlay className="w-[400px]">
|
||||||
|
<source src={url} type="video/mp4" />
|
||||||
|
<source src={url} type="video/webm" />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
props.filename.endsWith(".png") ||
|
props.filename.endsWith(".png") ||
|
||||||
props.filename.endsWith(".gif") ||
|
props.filename.endsWith(".gif") ||
|
||||||
@@ -12,13 +26,13 @@ export async function OutputRender(props: {
|
|||||||
props.filename.endsWith(".jpeg")
|
props.filename.endsWith(".jpeg")
|
||||||
) {
|
) {
|
||||||
const url = await getFileDownloadUrl(
|
const url = await getFileDownloadUrl(
|
||||||
`outputs/runs/${props.run_id}/${props.filename}`
|
`outputs/runs/${props.run_id}/${props.filename}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return <img className="max-w-[200px]" alt={props.filename} src={url} />;
|
return <img className="max-w-[200px]" alt={props.filename} src={url} />;
|
||||||
} else {
|
} else {
|
||||||
const url = await getFileDownloadUrl(
|
const url = await getFileDownloadUrl(
|
||||||
`outputs/runs/${props.run_id}/${props.filename}`
|
`outputs/runs/${props.run_id}/${props.filename}`,
|
||||||
);
|
);
|
||||||
// console.log(url);
|
// console.log(url);
|
||||||
|
|
||||||
|
|||||||
@@ -124,14 +124,48 @@ export function MachineSelect({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelectedMachineStore = {
|
||||||
|
selectedMachine: string | undefined;
|
||||||
|
setSelectedMachine: (machine: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectedMachineStore = create<SelectedMachineStore>((set) => ({
|
||||||
|
selectedMachine: undefined,
|
||||||
|
setSelectedMachine: (machine) => set(() => ({ selectedMachine: machine })),
|
||||||
|
}));
|
||||||
|
|
||||||
export function useSelectedMachine(
|
export function useSelectedMachine(
|
||||||
machines: Awaited<ReturnType<typeof getMachines>>,
|
machines: Awaited<ReturnType<typeof getMachines>>,
|
||||||
) {
|
): [string, (v: string) => void] {
|
||||||
const a = useQueryState("machine", {
|
const { selectedMachine, setSelectedMachine } = selectedMachineStore();
|
||||||
defaultValue: machines?.[0]?.id ?? "",
|
return [selectedMachine ?? machines?.[0]?.id ?? "", setSelectedMachine];
|
||||||
});
|
|
||||||
|
|
||||||
return a;
|
// const searchParams = useSearchParams();
|
||||||
|
// const pathname = usePathname();
|
||||||
|
// const router = useRouter();
|
||||||
|
|
||||||
|
// const createQueryString = useCallback(
|
||||||
|
// (name: string, value: string) => {
|
||||||
|
// const params = new URLSearchParams(searchParams.toString());
|
||||||
|
// params.set(name, value);
|
||||||
|
|
||||||
|
// return params.toString();
|
||||||
|
// },
|
||||||
|
// [searchParams],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return [
|
||||||
|
// searchParams.get("machine") ?? machines?.[0]?.id ?? "",
|
||||||
|
// (v: string) => {
|
||||||
|
// // window.history.pushState(
|
||||||
|
// // "new url",
|
||||||
|
// // "",
|
||||||
|
// // pathname + "?" + createQueryString("machine", v),
|
||||||
|
// // );
|
||||||
|
// // router.push(pathname + "?" + createQueryString("machine", v));
|
||||||
|
// router.replace(pathname + "?" + createQueryString("machine", v));
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublicRunStore = {
|
type PublicRunStore = {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export const registerWorkflowUploadRoute = (app: App) => {
|
|||||||
workflow_id = _workflow_id;
|
workflow_id = _workflow_id;
|
||||||
version = _version;
|
version = _version;
|
||||||
} else if (workflow_id) {
|
} else if (workflow_id) {
|
||||||
const workflow = await db
|
const _workflow = await db
|
||||||
.select()
|
.select()
|
||||||
.from(workflowTable)
|
.from(workflowTable)
|
||||||
.where(
|
.where(
|
||||||
@@ -126,7 +126,7 @@ export const registerWorkflowUploadRoute = (app: App) => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (workflow.length === 0) {
|
if (_workflow.length === 0) {
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
error: "Invalid workflow_id",
|
error: "Invalid workflow_id",
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ export const createRun = withServerPromise(
|
|||||||
if (node.inputs["input_id"] === key) {
|
if (node.inputs["input_id"] === key) {
|
||||||
node.inputs["input_id"] = inputs[key];
|
node.inputs["input_id"] = inputs[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix for external text default value
|
||||||
|
if (node.class_type == "ComfyUIDeployExternalText") {
|
||||||
|
node.inputs["default_value"] = inputs[key];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ export async function createDeployments(
|
|||||||
machine_id,
|
machine_id,
|
||||||
environment,
|
environment,
|
||||||
org_id: orgId,
|
org_id: orgId,
|
||||||
share_slug: slugify(`${userName} ${workflow.name}`),
|
// only create share slug if this is public share
|
||||||
|
share_slug: environment == "public-share" ? slugify(`${userName} ${workflow.name}`) : null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
revalidatePath(`/${workflow_id}`);
|
revalidatePath(`/${workflow_id}`);
|
||||||
|
|||||||
@@ -49,24 +49,26 @@ export async function getRunsData(run_id: string, user?: APIKeyUserType) {
|
|||||||
for (let i = 0; i < data.outputs.length; i++) {
|
for (let i = 0; i < data.outputs.length; i++) {
|
||||||
const output = data.outputs[i];
|
const output = data.outputs[i];
|
||||||
|
|
||||||
if (output.data?.images !== undefined) {
|
if (output.data?.images !== undefined)
|
||||||
for (let j = 0; j < output.data?.images.length; j++) {
|
replaceUrls(output.data?.images, data.id);
|
||||||
const element = output.data?.images[j];
|
|
||||||
element.url = replaceCDNUrl(
|
if (output.data?.files !== undefined)
|
||||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${data.id}/${element.filename}`
|
replaceUrls(output.data?.files, data.id);
|
||||||
);
|
|
||||||
}
|
if (output.data?.gifs !== undefined)
|
||||||
} else if (output.data?.files !== undefined) {
|
replaceUrls(output.data?.gifs, data.id);
|
||||||
for (let j = 0; j < output.data?.files.length; j++) {
|
|
||||||
const element = output.data?.files[j];
|
|
||||||
element.url = replaceCDNUrl(
|
|
||||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${data.id}/${element.filename}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceUrls(dataType: any[], dataId: string) {
|
||||||
|
for (let j = 0; j < dataType.length; j++) {
|
||||||
|
const element = dataType[j];
|
||||||
|
element.url = replaceCDNUrl(
|
||||||
|
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${dataId}/${element.filename}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user