Compare commits

..
Author SHA1 Message Date
Nicholas Koben Kao ed659ebdb8 schema 2024-01-22 19:05:24 -08:00
Nicholas Koben Kao f6a1b88dda rest 2024-01-22 19:05:18 -08:00
Nicholas Koben Kao fed7b380b6 work 2024-01-22 17:23:35 -08:00
Nicholas Koben Kao 90cec6b778 schema 2024-01-20 22:01:11 -08:00
Nicholas Koben Kao 7b61fea849 volume work 2024-01-20 19:54:35 -08:00
Nicholas Koben Kao 60471a8e01 model in voluems 2024-01-20 12:53:48 -08:00
100 changed files with 2686 additions and 4238 deletions
-18
View File
@@ -1,18 +0,0 @@
ARG VARIANT=18-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && 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
-21
View File
@@ -1,21 +0,0 @@
{
"name": "Comfy Deploy Dev",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"postCreateCommand": "cd web && bun install && bun run migrate-local",
"remoteUser": "root",
"customizations": {
"vscode": {
"extensions": [
"biomejs.biome",
"formulahendry.auto-rename-tag",
"bradlc.vscode-tailwindcss",
"stivo.tailwind-fold",
"streetsidesoftware.code-spell-checker",
"GitHub.copilot",
"ms-azuretools.vscode-docker"
]
}
}
}
-48
View File
@@ -1,48 +0,0 @@
version: '3'
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
VSCODE_DEV_CONTAINER: true
volumes:
# Forwards the local Docker socket to the container.
- /var/run/docker.sock:/var/run/docker-host.sock
# Update this to wherever you want VS Code to mount the folder of your project
- ../..:/workspaces:cached
# Overrides default command so things don't shut down after the process ends.
# entrypoint: /usr/local/share/docker-init.sh
command: sleep infinity
postgres:
image: "postgres:15.2-alpine"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: verceldb
ports:
- "5480:5432"
pg_proxy:
image: ghcr.io/neondatabase/wsproxy:latest
environment:
APPEND_PORT: "postgres:5432"
ALLOW_ADDR_REGEX: ".*"
LOG_TRAFFIC: "true"
ports:
- "5481:80"
depends_on:
- postgres
localstack:
image: localstack/localstack:latest
environment:
SERVICES: s3
ports:
- 4566:4566
volumes:
- ../web/aws:/etc/localstack/init/ready.d
- ../web/aws:/app/web/aws
+8 -1
View File
@@ -1,7 +1,14 @@
{
"recommendations": [
"DavidAnson.vscode-markdownlint", // markdown linting
"yzhang.markdown-all-in-one", // nicer markdown support
"esbenp.prettier-vscode", // prettier plugin
"dbaeumer.vscode-eslint", // eslint plugin
"bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
"ban.spellright", // Spell check for docs
"biomejs.biome" // improved i18n DX
"stripe.vscode-stripe", // stripe VSCode extension
"Prisma.prisma", // syntax|format|completion for prisma
"rebornix.project-snippets", // Share useful snippets between collaborators
"inlang.vs-code-extension" // improved i18n DX
]
}
+7 -5
View File
@@ -1,16 +1,18 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"quickfix.biome": "always"
// "source.organizeImports.biome": "explicit"
"source.fixAll.eslint": true
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
[
"cva\\(([^)]*)\\)",
"[\"'`]([^\"'`]*).*?[\"'`]"
]
],
// "eslint.workingDirectories": [
// { "mode": "auto" }
// ],
+2 -3
View File
@@ -75,11 +75,10 @@ Major areas
3. `bun i`
4. Start docker
5. `cp .env.example .env.local`
6. Replace `JWT_SECRET` with `openssl rand -hex 32`
6. Repace `JWT_SECRET` with `openssl rand -hex 32`
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`
9. Execute the local migration to create the initial data `bun run migrate-local`
10. Finally start the next server with `bun dev`
9. Finally start the next server with `bun dev`
**Schema Changes**
-28
View File
@@ -1,28 +0,0 @@
{
"$schema": "./web/node_modules/@biomejs/biome/configuration_schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"enabled": true
},
"linter": {
"enabled": false,
"rules": {
"recommended": true
}
},
"json": {
"parser": {
"allowComments": true
}
},
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true,
"defaultBranch": "main"
}
}
+50 -1
View File
@@ -8,6 +8,7 @@ from enum import Enum
import json
import subprocess
import time
from uuid import uuid4
from contextlib import asynccontextmanager
import asyncio
import threading
@@ -19,6 +20,7 @@ from urllib.parse import parse_qs
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp, Scope, Receive, Send
from concurrent.futures import ThreadPoolExecutor
# executor = ThreadPoolExecutor(max_workers=5)
@@ -224,6 +226,52 @@ async def websocket_endpoint(websocket: WebSocket, machine_id: str):
# return {"Hello": "World"}
class UploadBody(BaseModel):
download_url: str
volume_name: str
volume_id: str
# callback_url: str
@app.post("/upload_volume")
async def upload_checkpoint(body: UploadBody):
global last_activity_time
last_activity_time = time.time()
logger.info(f"Extended inactivity time to {global_timeout}")
download_url = body.download_url
volume_name = body.volume_name
# callback_url = body.callback_url
folder_path = f"/app/builds/{body.volume_id}"
cp_process = await asyncio.subprocess.create_subprocess_exec("cp", "-r", "/app/src/volume-builder", folder_path)
await cp_process.wait()
# Write the config file
config = {
"volume_names": {
volume_name: download_url
},
"paths": {
volume_name: f'/volumes/{uuid4()}'
},
}
await asyncio.subprocess.create_subprocess_shell(
f"modal run app.py",
# stdout=asyncio.subprocess.PIPE,
# stderr=asyncio.subprocess.PIPE,
cwd=folder_path,
env={**os.environ, "COLUMNS": "10000"}
)
with open(f"{folder_path}/config.py", "w") as f:
f.write("config = " + json.dumps(config))
# check that thi
return JSONResponse(status_code=200, content={"message": "Volume uploading", "build_machine_instance_id": fly_instance_id})
@app.post("/create")
async def create_machine(item: Item):
global last_activity_time
@@ -313,7 +361,8 @@ async def build_logic(item: Item):
"name": item.name,
"deploy_test": os.environ.get("DEPLOY_TEST_FLAG", "False"),
"gpu": item.gpu,
"civitai_token": os.environ.get("CIVITAI_TOKEN", "")
"public_checkpoint_volume": "model-store",
"private_checkpoint_volume": "private-model-store"
}
with open(f"{folder_path}/config.py", "w") as f:
f.write("config = " + json.dumps(config))
+13 -12
View File
@@ -1,12 +1,13 @@
from config import config
import modal
from modal import Image, Mount, web_endpoint, Stub, asgi_app
from modal import Image, Mount, web_endpoint, Stub, asgi_app, Volume
import json
import urllib.request
import urllib.parse
from pydantic import BaseModel
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from volume import volumes
# deploy_test = False
@@ -28,7 +29,6 @@ web_app = FastAPI()
print(config)
print("deploy_test ", deploy_test)
stub = Stub(name=config["name"])
# print(stub.app_id)
if not deploy_test:
# dockerfile_image = Image.from_dockerfile(f"{current_directory}/Dockerfile", context_mount=Mount.from_local_dir(f"{current_directory}/data", remote_path="/data"))
@@ -36,9 +36,6 @@ if not deploy_test:
dockerfile_image = (
modal.Image.debian_slim()
.env({
"CIVITAI_TOKEN": config["civitai_token"],
})
.apt_install("git", "wget")
.pip_install(
"git+https://github.com/modal-labs/asgiproxy.git", "httpx", "tqdm"
@@ -59,7 +56,7 @@ if not deploy_test:
# # Install comfy deploy
# "cd /comfyui/custom_nodes && git clone https://github.com/BennyKok/comfyui-deploy.git",
# )
# .copy_local_file(f"{current_directory}/data/extra_model_paths.yaml", "/comfyui")
.copy_local_file(f"{current_directory}/data/extra_model_paths.yaml", "/comfyui")
.copy_local_file(f"{current_directory}/data/start.sh", "/start.sh")
.run_commands("chmod +x /start.sh")
@@ -156,8 +153,9 @@ image = Image.debian_slim()
target_image = image if deploy_test else dockerfile_image
@stub.function(image=target_image, gpu=config["gpu"])
@stub.function(image=target_image, gpu=config["gpu"]
,volumes=volumes
)
def run(input: Input):
import subprocess
import time
@@ -166,6 +164,7 @@ def run(input: Input):
command = ["python", "main.py",
"--disable-auto-launch", "--disable-metadata"]
server_process = subprocess.Popen(command, cwd="/comfyui")
check_server(
@@ -234,12 +233,13 @@ def run(input: Input):
async def bar(request_input: RequestInput):
# print(request_input)
if not deploy_test:
run.spawn(request_input.input)
return {"status": "success"}
return run.remote(request_input.input)
# pass
@stub.function(image=image)
@stub.function(image=image
,volumes=volumes
)
@asgi_app()
def comfyui_api():
return web_app
@@ -289,6 +289,7 @@ def spawn_comfyui_in_background():
# to be on a single container.
concurrency_limit=1,
timeout=10 * 60,
volumes=volumes,
)
@asgi_app()
def comfyui_app():
@@ -307,4 +308,4 @@ def comfyui_app():
},
)()
return make_simple_proxy_app(ProxyContext(config))
return make_simple_proxy_app(ProxyContext(config))
+7 -1
View File
@@ -1 +1,7 @@
config = {"name": "my-app", "deploy_test": "True", "gpu": "T4"}
config = {
"name": "my-app",
"deploy_test": "True",
"gpu": "T4",
"public_checkpoint_volume": "model-store",
"private_checkpoint_volume": "private-model-store"
}
@@ -1,11 +1,30 @@
comfyui:
base_path: /runpod-volume/ComfyUI/
checkpoints: models/checkpoints/
clip: models/clip/
clip_vision: models/clip_vision/
configs: models/configs/
controlnet: models/controlnet/
embeddings: models/embeddings/
loras: models/loras/
upscale_models: models/upscale_models/
vae: models/vae/
base_path: /extra_models/
checkpoints: |
checkpoints
private_checkpoints
clip: |
clip
private_clip
clip_vision: |
clip_vision
private_clip_vision
configs: |
configs
private_configs
controlnet: |
controlnet
private_controlnet
embeddings: |
embeddings
private_embeddings
loras: |
loras
private_loras
upscale_models: |
upscale_models
private_upscale_models
vae: |
vae
private_vae
@@ -0,0 +1,101 @@
"""
This is a standalone script to download models into a modal Volume using civitai
Example Usage
`modal run insert_models::insert_model --civitai-url https://civitai.com/models/36520/ghostmix`
This inserts an individual model from a civitai url
`modal run insert_models::insert_models_civitai_api`
This inserts a bunch of models based on the models retrieved by civitai
civitai's API reference https://github.com/civitai/civitai/wiki/REST-API-Reference
"""
import modal
import subprocess
import requests
import json
stub = modal.Stub()
# NOTE: volume name can be variable
volume = modal.Volume.persisted("rah")
model_store_path = "/vol/models"
MODEL_ROUTE = "models"
image = (
modal.Image.debian_slim().apt_install("wget").pip_install("requests")
)
@stub.function(volumes={model_store_path: volume}, image=image, timeout=50000, gpu=None)
def download_model(download_url):
print(download_url)
subprocess.run(["wget", download_url, "--content-disposition", "-P", model_store_path])
subprocess.run(["ls", "-la", model_store_path])
volume.commit()
# file is raw output from Civitai API https://github.com/civitai/civitai/wiki/REST-API-Reference
@stub.function()
def get_civitai_models(model_type: str, sort: str = "Highest Rated", page: int = 1):
"""Fetch models from CivitAI API based on type."""
try:
response = requests.get(f"https://civitai.com/api/v1/models", params={"types": model_type, "page": page, "sort": sort})
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"Error fetching models: {e}")
return None
@stub.function()
def get_civitai_model_url(civitai_url: str):
# Validate the URL
if civitai_url.startswith("https://civitai.com/api/"):
api_url = civitai_url
elif civitai_url.startswith("https://civitai.com/models/"):
try:
model_id = civitai_url.split("/")[4]
int(model_id)
except (IndexError, ValueError):
return None
api_url = f"https://civitai.com/api/v1/models/{model_id}"
else:
return "Error: URL must be from civitai.com and contain /models/"
response = requests.get(api_url)
# Check for successful response
if response.status_code != 200:
return f"Error: Unable to fetch data from {api_url}"
# Return the response data
return response.json()
@stub.local_entrypoint()
def insert_models_civitai_api(type: str = "Checkpoint", sort = "Highest Rated", page: int = 1):
civitai_models = get_civitai_models.local(type, sort, page)
if civitai_models:
for _ in download_model.map(map(lambda model: model['modelVersions'][0]['downloadUrl'], civitai_models['items'])):
pass
else:
print("Failed to retrieve models.")
@stub.local_entrypoint()
def insert_model(civitai_url: str):
if civitai_url.startswith("'https://civitai.com/api/download/models/"):
download_url = civitai_url
else:
civitai_model = get_civitai_model_url.local(civitai_url)
if civitai_model:
download_url = civitai_model['modelVersions'][0]['downloadUrl']
else:
return "invalid URL"
download_model.remote(download_url)
@stub.local_entrypoint()
def simple_download():
download_urls = ['https://civitai.com/api/download/models/119057', 'https://civitai.com/api/download/models/130090', 'https://civitai.com/api/download/models/31859', 'https://civitai.com/api/download/models/128713', 'https://civitai.com/api/download/models/179657', 'https://civitai.com/api/download/models/143906', 'https://civitai.com/api/download/models/9208', 'https://civitai.com/api/download/models/136078', 'https://civitai.com/api/download/models/134065', 'https://civitai.com/api/download/models/288775', 'https://civitai.com/api/download/models/95263', 'https://civitai.com/api/download/models/288982', 'https://civitai.com/api/download/models/87153', 'https://civitai.com/api/download/models/10638', 'https://civitai.com/api/download/models/263809', 'https://civitai.com/api/download/models/130072', 'https://civitai.com/api/download/models/117019', 'https://civitai.com/api/download/models/95256', 'https://civitai.com/api/download/models/197181', 'https://civitai.com/api/download/models/256915', 'https://civitai.com/api/download/models/118945', 'https://civitai.com/api/download/models/125843', 'https://civitai.com/api/download/models/179015', 'https://civitai.com/api/download/models/245598', 'https://civitai.com/api/download/models/223670', 'https://civitai.com/api/download/models/90072', 'https://civitai.com/api/download/models/290817', 'https://civitai.com/api/download/models/154097', 'https://civitai.com/api/download/models/143497', 'https://civitai.com/api/download/models/5637']
for _ in download_model.map(download_urls):
pass
@@ -45,19 +45,13 @@ for package in packages:
response = requests.request("POST", f"{root_url}/customnode/install", json=package, headers=headers)
print(response.text)
with open('models.json') as f:
models = json.load(f)
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)
print(response.text)
# with open('models.json') as f:
# models = json.load(f)
#
# for model in models:
# response = requests.request("POST", f"{root_url}/model/install", json=model, headers=headers)
# print(response.text)
# Close the server
server_process.terminate()
print("Finished installing dependencies.")
print("Finished installing dependencies.")
@@ -0,0 +1,10 @@
import modal
from config import config
public_model_volume = modal.Volume.persisted(config["public_checkpoint_volume"])
private_volume = modal.Volume.persisted(config["private_checkpoint_volume"])
BASEMODEL_DIR = "/extra_models/"
MODEL_DIR = BASEMODEL_DIR + "checkpoints"
PRIVATE_MODEL_DIR = BASEMODEL_DIR + "private_checkpoints"
volumes = {MODEL_DIR: public_model_volume, PRIVATE_MODEL_DIR: private_volume}
@@ -0,0 +1,45 @@
import modal
from config import config
import os
import subprocess
stub = modal.Stub()
# Volume names may only contain alphanumeric characters, dashes, periods, and underscores, and must be less than 64 characters in length.
def is_valid_name(name: str) -> bool:
allowed_characters = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._")
return 0 < len(name) <= 64 and all(char in allowed_characters for char in name)
def create_volumes(volume_names, paths):
path_to_vol = {}
for volume_name in volume_names.keys():
if not is_valid_name(volume_name):
pass
modal_volume = modal.Volume.persisted(volume_name)
path_to_vol[paths[volume_name]] = modal_volume
return path_to_vol
vol_name_to_links = config["volume_names"]
vol_name_to_path = config["paths"]
volumes = create_volumes(vol_name_to_links, vol_name_to_path)
image = (
modal.Image.debian_slim().apt_install("wget").pip_install("requests")
)
print(vol_name_to_links)
print(vol_name_to_path)
print(volumes)
@stub.function(volumes=volumes, image=image, timeout=5000, gpu=None)
def download_model(volume_name, download_url):
model_store_path = vol_name_to_path[volume_name]
subprocess.run(["wget", download_url, "--content-disposition", "-P", model_store_path])
subprocess.run(["ls", "-la", model_store_path])
volumes[model_store_path].commit()
@stub.local_entrypoint()
def simple_download():
print(vol_name_to_links)
print([(vol_name, link) for vol_name,link in vol_name_to_links.items()])
list(download_model.starmap([(vol_name, link) for vol_name,link in vol_name_to_links.items()]))
@@ -0,0 +1,8 @@
config = {
"volume_names": {
"test": "https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose (1).png"
},
"paths": {
"test": "/volumes/something"
}
}
-68
View File
@@ -1,68 +0,0 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
import folder_paths
from tqdm import tqdm
class ComfyUIDeployExternalCheckpoint:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_checkpoint"},
),
},
"optional": {
"default_checkpoint_name": (folder_paths.get_filename_list("checkpoints"), ),
}
}
RETURN_TYPES = (folder_paths.get_filename_list("checkpoints"),)
RETURN_NAMES = ("path",)
FUNCTION = "run"
CATEGORY = "deploy"
def run(self, input_id, default_checkpoint_name=None):
import requests
import os
import uuid
if input_id and input_id.startswith('http'):
unique_filename = str(uuid.uuid4()) + ".safetensors"
print(unique_filename)
print(folder_paths.folder_names_and_paths["checkpoints"][0][0])
destination_path = os.path.join(
folder_paths.folder_names_and_paths["checkpoints"][0][0], unique_filename)
print(destination_path)
print("Downloading external checkpoint - " +
input_id + " to " + destination_path)
response = requests.get(
input_id, headers={'User-Agent': 'Mozilla/5.0'}, allow_redirects=True, stream=True)
file_size = int(response.headers.get('Content-Length', 0))
chunk = 1
chunk_size = 1024
num_bars = int(file_size / chunk_size)
with open(destination_path, 'wb') as out_file:
for chunk in tqdm(
response.iter_content(chunk_size=chunk_size),
total=num_bars,
unit='KB',
desc="Downloading",
leave=True # leave=True to keep progress bars
):
out_file.write(chunk)
return (unique_filename,)
else:
return (default_checkpoints_name,)
NODE_CLASS_MAPPINGS = {
"ComfyUIDeployExternalCheckpoint": ComfyUIDeployExternalCheckpoint}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyUIDeployExternalCheckpoint": "External Checkpoint (ComfyUI Deploy)"}
+3 -1
View File
@@ -29,7 +29,9 @@ class ComfyUIDeployExternalText:
CATEGORY = "text"
def run(self, input_id, default_value=None):
return [default_value]
if not input_id or len(input_id.strip()) == 0:
return [default_value]
return [input_id]
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalText": ComfyUIDeployExternalText}
+17 -234
View File
@@ -22,14 +22,10 @@ from logging.handlers import RotatingFileHandler
from enum import Enum
from urllib.parse import quote
import threading
import hashlib
import aiohttp
api = None
api_task = None
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):
prompt_server = server.PromptServer.instance
@@ -101,7 +97,6 @@ async def comfy_deploy_run(request):
prompt_metadata[prompt_id] = {
'status_endpoint': data.get('status_endpoint'),
'file_upload_endpoint': data.get('file_upload_endpoint'),
'workflow_api': workflow_api
}
try:
@@ -118,8 +113,6 @@ async def comfy_deploy_run(request):
"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}")
status = 200
@@ -148,138 +141,6 @@ async def comfy_deploy_run(request):
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')
async def websocket_handler(request):
ws = web.WebSocketResponse()
@@ -296,9 +157,7 @@ async def websocket_handler(request):
try:
# Send initial state to the new client
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:
if msg.type == aiohttp.WSMsgType.ERROR:
@@ -344,10 +203,7 @@ async def send_json_override(self, event, data, sid=None):
prompt_id = data.get('prompt_id')
# now we send everything
await asyncio.wait([
asyncio.create_task(send(event, data)),
asyncio.create_task(self.send_json_original(event, data, sid))
])
await asyncio.wait([send(event, data), self.send_json_original(event, data, sid)])
if event == 'execution_start':
update_run(prompt_id, Status.RUNNING)
@@ -358,17 +214,6 @@ async def send_json_override(self, event, data, sid=None):
if not have_pending_upload(prompt_id):
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':
# Careful this might not be fully awaited.
await update_run_with_output(prompt_id, data)
@@ -388,26 +233,7 @@ class Status(Enum):
FAILED = "failed"
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):
global last_read_line_number
if prompt_id not in prompt_metadata:
return
@@ -422,50 +248,16 @@ def update_run(prompt_id, status: Status):
"run_id": prompt_id,
"status": status.value,
}
prompt_metadata[prompt_id]['status'] = status
print(f"Status: {status.value}")
try:
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:
error_type = type(e).__name__
stack_trace = traceback.format_exc().strip()
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"):
"""
@@ -515,9 +307,7 @@ async def upload_file(prompt_id, filename, subfolder=None, content_type="image/p
"Content-Length": str(len(data)),
}
response = requests.put(ok.get("url"), headers=headers, data=data)
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)
print("upload file response", response.status_code)
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:
@@ -594,25 +384,20 @@ async def update_file_status(prompt_id, data, uploading, have_error=False, node_
"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
async def upload_in_background(prompt_id, data, node_id=None, have_upload=True):
try:
await handle_upload(prompt_id, data, 'images', "content_type", "image/png")
await handle_upload(prompt_id, data, 'files', "content_type", "image/png")
# This will also be mp4
await handle_upload(prompt_id, data, 'gifs', "format", "image/gif")
images = data.get('images', [])
for image in images:
await upload_file(prompt_id, image.get("filename"), subfolder=image.get("subfolder"), type=image.get("type"), content_type=image.get("content_type", "image/png"))
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:
await update_file_status(prompt_id, data, False, node_id=node_id)
@@ -653,7 +438,6 @@ prompt_server.send_json = send_json_override.__get__(prompt_server, server.Promp
root_path = os.path.dirname(os.path.abspath(__file__))
two_dirs_up = os.path.dirname(os.path.dirname(root_path))
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
@@ -693,5 +477,4 @@ def run_in_new_thread(coroutine):
t.start()
asyncio.run_coroutine_threadsafe(coroutine, new_loop)
if cd_enable_log:
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
+32 -43
View File
@@ -7,56 +7,45 @@ import threading
import logging
from logging.handlers import RotatingFileHandler
# Running with export CD_ENABLE_LOG=true; python main.py
handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5)
# Check for 'cd-enable-log' flag in input arguments
# cd_enable_log = '--cd-enable-log' in sys.argv
cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true'
original_stdout = sys.stdout
original_stderr = sys.stderr
def setup():
handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5)
class StreamToLogger():
def __init__(self, log_level):
self.log_level = log_level
original_stdout = sys.stdout
original_stderr = sys.stderr
def write(self, buf):
if (self.log_level == logging.INFO):
original_stdout.write(buf)
original_stdout.flush()
elif (self.log_level == logging.ERROR):
original_stderr.write(buf)
original_stderr.flush()
class StreamToLogger():
def __init__(self, log_level):
self.log_level = log_level
def write(self, buf):
if (self.log_level == logging.INFO):
original_stdout.write(buf)
original_stdout.flush()
elif (self.log_level == logging.ERROR):
original_stderr.write(buf)
original_stderr.flush()
for line in buf.rstrip().splitlines():
handler.handle(
logging.LogRecord(
name="comfy-deploy",
level=self.log_level,
pathname="prestartup_script.py",
lineno=1,
msg=line.rstrip(),
args=None,
exc_info=None
)
for line in buf.rstrip().splitlines():
handler.handle(
logging.LogRecord(
name="comfy-deploy",
level=self.log_level,
pathname="prestartup_script.py",
lineno=1,
msg=line.rstrip(),
args=None,
exc_info=None
)
)
def flush(self):
if (self.log_level == logging.INFO):
original_stdout.flush()
elif (self.log_level == logging.ERROR):
original_stderr.flush()
def flush(self):
if (self.log_level == logging.INFO):
original_stdout.flush()
elif (self.log_level == logging.ERROR):
original_stderr.flush()
# Redirect stdout and stderr to the logger
sys.stdout = StreamToLogger(logging.INFO)
sys.stderr = StreamToLogger(logging.ERROR)
if cd_enable_log:
print("** Comfy Deploy logging enabled")
setup()
# Redirect stdout and stderr to the logger
sys.stdout = StreamToLogger(logging.INFO)
sys.stderr = StreamToLogger(logging.ERROR)
try:
# Get the absolute path of the script's directory
+49 -429
View File
@@ -1,7 +1,6 @@
import { app } from "./app.js";
import { api } from "./api.js";
import { ComfyWidgets, LGraphNode } from "./widgets.js";
import { generateDependencyGraph } from "https://esm.sh/[email protected]";
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
/** @type {ComfyExtension} */
@@ -10,76 +9,6 @@ const ext = {
init(app) {
addButton();
const queryParams = new URLSearchParams(window.location.search);
const workflow_version_id = queryParams.get("workflow_version_id");
const auth_token = queryParams.get("auth_token");
const org_display = queryParams.get("org_display");
const origin = queryParams.get("origin");
const data = getData();
let endpoint = data.endpoint;
let apiKey = data.apiKey;
// If there is auth token override it
if (auth_token) {
apiKey = auth_token;
endpoint = origin;
saveData({
displayName: org_display,
endpoint: origin,
apiKey: auth_token,
displayName: org_display,
environment: "cloud",
});
localStorage.setItem("comfy_deploy_env", "cloud");
}
if (!workflow_version_id) {
console.error("No workflow_version_id provided in query parameters.");
} else {
loadingDialog.showLoading(
"Loading workflow from " + org_display,
"Please wait...",
);
fetch(endpoint + "/api/workflow-version/" + workflow_version_id, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
})
.then(async (res) => {
const data = await res.json();
const { workflow, workflow_id, error } = data;
if (error) {
infoDialog.showMessage("Unable to load this workflow", error);
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} */
app.loadGraphData(workflow);
})
.catch((e) => infoDialog.showMessage("Error", e.message))
.finally(() => {
loadingDialog.close();
window.history.replaceState(
{},
document.title,
window.location.pathname,
);
});
}
},
registerCustomNodes() {
@@ -179,92 +108,6 @@ 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() {
const menu = document.querySelector(".comfy-menu");
@@ -276,29 +119,8 @@ function addButton() {
/** @type {LGraph} */
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());
// console.log(snapshot);
loadingDialog.close();
console.log(snapshot);
if (!snapshot) {
showError(
@@ -332,122 +154,52 @@ function addButton() {
const deployMetaNode = deployMeta[0];
console.log(deployMetaNode);
const workflow_name = deployMetaNode.widgets[0].value;
const workflow_id = deployMetaNode.widgets[1].value;
console.log(workflow_name, workflow_id);
const prompt = await app.graphToPrompt();
let deps = undefined;
console.log(graph);
console.log(prompt);
if (includeDeps) {
loadingDialog.showLoading("Fetching existing version", "Please wait...");
// const endpoint = localStorage.getItem("endpoint") ?? "";
// const apiKey = localStorage.getItem("apiKey");
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 {};
});
const { endpoint, apiKey } = getData();
loadingDialog.close();
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);
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
configDialog.show();
return;
}
loadingDialog.showLoading("Deploying...");
const ok = await confirmDialog.confirm("Confirm deployment", "A new version will be deployed, are you conform?")
if (!ok) return;
title.innerText = "Deploying...";
title.style.color = "orange";
// console.log(prompt);
console.log(prompt);
// TODO trim the ending / from endpoint is there is
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
// console.log(prompt.workflow);
const apiRoute = endpoint + "/api/workflow";
const apiRoute = endpoint + "/api/upload";
// const userId = apiKey
try {
const body = {
workflow_name,
workflow_id,
workflow: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
dependencies: deps,
};
console.log(body);
let data = await fetch(apiRoute, {
method: "POST",
body: JSON.stringify(body),
body: JSON.stringify({
workflow_name,
workflow_id,
workflow: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
}),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
@@ -462,8 +214,6 @@ function addButton() {
data = await data.json();
}
loadingDialog.close();
title.textContent = "Done";
title.style.color = "green";
@@ -480,7 +230,6 @@ function addButton() {
title.style.color = "white";
}, 1000);
} catch (e) {
loadingDialog.close();
app.ui.dialog.show(e);
console.error(e);
title.textContent = "Error";
@@ -526,16 +275,14 @@ export class InfoDialog extends ComfyDialog {
this.element.classList.add("comfy-normal-modal");
this.element.style.paddingBottom = "20px";
}
button = undefined;
createButtons() {
this.button = $el("button", {
type: "button",
textContent: "Close",
onclick: () => this.close(),
});
return [this.button];
return [
$el("button", {
type: "button",
textContent: "Close",
onclick: () => this.close(),
}),
];
}
close() {
@@ -554,72 +301,6 @@ export class InfoDialog extends ComfyDialog {
this.element.style.display = "flex";
this.element.style.zIndex = 1001;
}
showMessage(title, message) {
this.show(`
<div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px;">${title}</h3>
<label>
${message}
</label>
</div>
`);
}
loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
showLoading(title, message) {
this.show(`
<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;">${title} ${this.loadingIcon}</h3>
<label>
${message}
</label>
</div>
`);
}
}
export class LoadingDialog extends ComfyDialog {
constructor() {
super();
this.element.classList.add("comfy-normal-modal");
// this.element.style.paddingBottom = "20px";
}
createButtons() {
return [];
}
close() {
this.element.style.display = "none";
}
show(html) {
this.textElement.style["white-space"] = "normal";
this.textElement.style.color = "white";
this.textElement.style.marginTop = "0px";
if (typeof html === "string") {
this.textElement.innerHTML = html;
} else {
this.textElement.replaceChildren(html);
}
this.element.style.display = "flex";
this.element.style.zIndex = 1001;
}
loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
showLoading(title, message) {
this.show(`
<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: 12px;">${title} ${
this.loadingIcon
}</h3>
${message ? `<label>${message}</label>` : ""}
</div>
`);
}
}
export class InputDialog extends InfoDialog {
@@ -686,6 +367,7 @@ export class InputDialog extends InfoDialog {
}
}
export class ConfirmDialog extends InfoDialog {
callback = undefined;
@@ -735,7 +417,7 @@ export class ConfirmDialog extends InfoDialog {
return new Promise((resolve, reject) => {
this.callback = resolve;
this.show(`
<div style="width: 100%; max-width: 600px; 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;">${title}</h3>
<label>
${message}
@@ -747,15 +429,9 @@ export class ConfirmDialog extends InfoDialog {
}
export const inputDialog = new InputDialog();
export const loadingDialog = new LoadingDialog();
export const infoDialog = new InfoDialog();
export const confirmDialog = new ConfirmDialog();
/**
* Retrieves deployment data from local storage or defaults.
* @param {string} [environment] - The environment to get the data for.
* @returns {{endpoint: string, apiKey: string, displayName: string, environment?: string}} The deployment data.
*/
function getData(environment) {
const deployOption =
environment || localStorage.getItem("comfy_deploy_env") || "cloud";
@@ -778,21 +454,8 @@ function getData(environment) {
};
}
/**
* Retrieves deployment data from local storage or defaults.
* @param {{endpoint: string, apiKey: string, displayName: string, environment?: string}} [data] - The environment to get the data for.
*/
function saveData(data) {
localStorage.setItem(
"comfy_deploy_env_data_" + data.environment,
JSON.stringify(data),
);
}
export class ConfigDialog extends ComfyDialog {
container = null;
poll = null;
timeout = null;
constructor() {
super();
@@ -835,24 +498,22 @@ export class ConfigDialog extends ComfyDialog {
close() {
this.element.style.display = "none";
clearInterval(this.poll);
clearTimeout(this.timeout);
}
save(api_key, displayName) {
if (!displayName) displayName = getData().displayName;
save() {
const deployOption = this.container.querySelector("#deployOption").value;
localStorage.setItem("comfy_deploy_env", deployOption);
const endpoint = this.container.querySelector("#endpoint").value;
const apiKey = api_key ?? this.container.querySelector("#apiKey").value;
saveData({
const apiKey = this.container.querySelector("#apiKey").value;
const data = {
endpoint,
apiKey,
displayName,
environment: deployOption,
});
};
localStorage.setItem(
"comfy_deploy_env_data_" + deployOption,
JSON.stringify(data),
);
this.close();
}
@@ -866,8 +527,12 @@ export class ConfigDialog extends ComfyDialog {
<h3 style="margin: 0px;">Comfy Deploy Config</h3>
<label style="color: white; width: 100%;">
<select id="deployOption" style="margin: 8px 0px; width: 100%; height:30px; box-sizing: border-box;" >
<option value="cloud" ${data.environment === "cloud" ? "selected" : ""}>Cloud</option>
<option value="local" ${data.environment === "local" ? "selected" : ""}>Local</option>
<option value="cloud" ${
data.environment === "cloud" ? "selected" : ""
}>Cloud</option>
<option value="local" ${
data.environment === "local" ? "selected" : ""
}>Local</option>
</select>
</label>
<label style="color: white; width: 100%;">
@@ -877,61 +542,16 @@ export class ConfigDialog extends ComfyDialog {
}">
</label>
<label style="color: white;">
API Key: ${data.displayName ?? ""}
API Key:
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${
data.apiKey
}">
<button id="loginButton" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;">
${
data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
}
</button>
</label>
</div>
`;
const button = this.container.querySelector("#loginButton");
button.onclick = () => {
const uuid =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
window.open(data.endpoint + "/auth-request/" + uuid, "_blank");
this.timeout = setTimeout(() => {
clearInterval(poll);
infoDialog.showMessage(
"Timeout",
"Wait too long for the response, please try re-login",
);
}, 30000); // Stop polling after 30 seconds
this.poll = setInterval(() => {
fetch(data.endpoint + "/api/auth-response/" + uuid)
.then((response) => response.json())
.then((json) => {
if (json.api_key) {
this.save(json.api_key, json.name);
this.container.querySelector("#apiKey").value = json.api_key;
infoDialog.show();
clearInterval(this.poll);
clearTimeout(this.timeout);
infoDialog.showMessage(
"Authenticated",
"You will be able to upload workflow to " + json.name,
);
}
})
.catch((error) => {
console.error("Error:", error);
clearInterval(this.poll);
clearTimeout(this.timeout);
infoDialog.showMessage("Error", error);
});
}, 2000);
};
const apiKeyInput = this.container.querySelector("#apiKey");
apiKeyInput.addEventListener("paste", (e) => {
apiKeyInput.addEventListener("paste", function (e) {
e.stopPropagation();
});
+6
View File
@@ -0,0 +1,6 @@
node_modules
**/node_modules
**/.next
**/public
packages/prisma/zod
apps/web/public/embed
+95
View File
@@ -0,0 +1,95 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: [
// "plugin:playwright/playwright-test",
"next",
// "next/core-web-vitals",
"plugin:prettier/recommended",
// "turbo",
// "plugin:you-dont-need-lodash-underscore/compatible-warn",
],
plugins: ["unused-imports"],
parserOptions: {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"],
// project: ["./apps/*/tsconfig.json", "./packages/*/tsconfig.json"],
},
settings: {
next: {
// rootDir: ["apps/*/", "packages/*/"],
rootDir: ["src"],
},
},
rules: {
"@next/next/no-img-element": "off",
"@next/next/no-html-link-for-pages": "off",
"jsx-a11y/role-supports-aria-props": "off", // @see https://github.com/vercel/next.js/issues/27989#issuecomment-897638654
// "playwright/no-page-pause": "error",
"react/jsx-curly-brace-presence": [
"error",
{ props: "never", children: "never" },
],
"react/self-closing-comp": ["error", { component: true, html: true }],
"@typescript-eslint/no-unused-vars": [
"warn",
{
vars: "all",
varsIgnorePattern: "^_",
args: "after-used",
argsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
},
],
"unused-imports/no-unused-imports": "error",
"no-restricted-imports": [
"error",
{
patterns: ["lodash"],
},
],
"prefer-template": "error",
},
overrides: [
{
files: ["*.ts", "*.tsx"],
extends: [
"plugin:@typescript-eslint/recommended",
// "plugin:@calcom/eslint/recommended",
],
plugins: [
"@typescript-eslint",
// "@calcom/eslint"
],
parser: "@typescript-eslint/parser",
rules: {
"@typescript-eslint/consistent-type-imports": [
"error",
{
prefer: "type-imports",
// TODO: enable this once prettier supports it
// fixStyle: "inline-type-imports",
fixStyle: "separate-type-imports",
disallowTypeAnnotations: false,
},
],
},
// overrides: [
// {
// files: ["**/playwright/**/*.{tsx,ts}"],
// rules: {
// "@typescript-eslint/no-unused-vars": "off",
// "no-undef": "off",
// },
// },
// ],
},
// {
// files: ["**/playwright/**/*.{js,jsx}"],
// rules: {
// "@typescript-eslint/no-unused-vars": "off",
// "no-undef": "off",
// },
// },
],
};
BIN
View File
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
ALTER TABLE "comfyui_deploy"."workflow_runs" ADD COLUMN "started_at" timestamp;
+62
View File
@@ -0,0 +1,62 @@
DO $$ BEGIN
CREATE TYPE "model_upload_type" AS ENUM('civitai', 'huggingface', 'other');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
CREATE TYPE "resource_upload" AS ENUM('started', 'failed', 'succeded');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "comfyui_deploy"."checkpoints" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" text,
"org_id" text,
"description" text,
"checkpoint_volume_id" uuid NOT NULL,
"model_name" text,
"civitai_id" text,
"civitai_version_id" text,
"civitai_url" text,
"civitai_download_url" text,
"civitai_model_response" jsonb,
"hf_url" text,
"s3_url" text,
"client_url" text,
"is_public" boolean DEFAULT false NOT NULL,
"status" "resource_upload" DEFAULT 'started' NOT NULL,
"upload_machine_id" text,
"upload_type" "model_upload_type" NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "comfyui_deploy"."checkpoint_volume" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" text,
"org_id" text,
"volume_name" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"disabled" boolean DEFAULT false NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "comfyui_deploy"."checkpoints" ADD CONSTRAINT "checkpoints_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "comfyui_deploy"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "comfyui_deploy"."checkpoints" ADD CONSTRAINT "checkpoints_checkpoint_volume_id_workflow_runs_id_fk" FOREIGN KEY ("checkpoint_volume_id") REFERENCES "comfyui_deploy"."workflow_runs"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "comfyui_deploy"."checkpoint_volume" ADD CONSTRAINT "checkpoint_volume_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "comfyui_deploy"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
+2
View File
@@ -0,0 +1,2 @@
ALTER TYPE "resource_upload" ADD VALUE 'error';--> statement-breakpoint
ALTER TABLE "comfyui_deploy"."checkpoints" ADD COLUMN "build_log" text;
-2
View File
@@ -1,2 +0,0 @@
ALTER TABLE "comfyui_deploy"."deployments" ADD COLUMN "share_slug" text;--> statement-breakpoint
ALTER TABLE "comfyui_deploy"."deployments" ADD CONSTRAINT "deployments_share_slug_unique" UNIQUE("share_slug");
-8
View File
@@ -1,8 +0,0 @@
CREATE TABLE IF NOT EXISTS "comfyui_deploy"."auth_requests" (
"request_id" text PRIMARY KEY NOT NULL,
"user_id" text,
"org_id" text,
"api_hash" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
-1
View File
@@ -1 +0,0 @@
ALTER TABLE "comfyui_deploy"."auth_requests" ADD COLUMN "expired_date" timestamp;
+249 -7
View File
@@ -1,5 +1,5 @@
{
"id": "1ca4fdb7-c0c4-4c39-8b47-f40282293da0",
"id": "4d5b29d0-848f-4c2e-a2cd-2932f1fa38c6",
"prevId": "db06ea66-92c2-4ebe-93c1-6cb8a90ccd8b",
"version": "5",
"dialect": "pg",
@@ -88,6 +88,238 @@
}
}
},
"checkpoints": {
"name": "checkpoints",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"checkpoint_volume_id": {
"name": "checkpoint_volume_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"model_name": {
"name": "model_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_id": {
"name": "civitai_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_version_id": {
"name": "civitai_version_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_url": {
"name": "civitai_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_download_url": {
"name": "civitai_download_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_model_response": {
"name": "civitai_model_response",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"hf_url": {
"name": "hf_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"s3_url": {
"name": "s3_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"client_url": {
"name": "client_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"is_public": {
"name": "is_public",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"status": {
"name": "status",
"type": "resource_upload",
"primaryKey": false,
"notNull": true,
"default": "'started'"
},
"upload_machine_id": {
"name": "upload_machine_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"upload_type": {
"name": "upload_type",
"type": "model_upload_type",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"checkpoints_user_id_users_id_fk": {
"name": "checkpoints_user_id_users_id_fk",
"tableFrom": "checkpoints",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"checkpoints_checkpoint_volume_id_workflow_runs_id_fk": {
"name": "checkpoints_checkpoint_volume_id_workflow_runs_id_fk",
"tableFrom": "checkpoints",
"tableTo": "workflow_runs",
"columnsFrom": [
"checkpoint_volume_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"checkpoint_volume": {
"name": "checkpoint_volume",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"volume_name": {
"name": "volume_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
"foreignKeys": {
"checkpoint_volume_user_id_users_id_fk": {
"name": "checkpoint_volume_user_id_users_id_fk",
"tableFrom": "checkpoint_volume",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
@@ -506,12 +738,6 @@
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
@@ -732,6 +958,22 @@
"comfy-deploy-serverless": "comfy-deploy-serverless"
}
},
"model_upload_type": {
"name": "model_upload_type",
"values": {
"civitai": "civitai",
"huggingface": "huggingface",
"other": "other"
}
},
"resource_upload": {
"name": "resource_upload",
"values": {
"started": "started",
"failed": "failed",
"succeded": "succeded"
}
},
"workflow_run_origin": {
"name": "workflow_run_origin",
"values": {
+257 -23
View File
@@ -1,6 +1,6 @@
{
"id": "1425ee00-66fb-4541-8da7-19b217944545",
"prevId": "1ca4fdb7-c0c4-4c39-8b47-f40282293da0",
"id": "fed3e81d-c0d7-4deb-a63a-370039fb5edc",
"prevId": "4d5b29d0-848f-4c2e-a2cd-2932f1fa38c6",
"version": "5",
"dialect": "pg",
"tables": {
@@ -88,6 +88,244 @@
}
}
},
"checkpoints": {
"name": "checkpoints",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"checkpoint_volume_id": {
"name": "checkpoint_volume_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"model_name": {
"name": "model_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_id": {
"name": "civitai_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_version_id": {
"name": "civitai_version_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_url": {
"name": "civitai_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_download_url": {
"name": "civitai_download_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_model_response": {
"name": "civitai_model_response",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"hf_url": {
"name": "hf_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"s3_url": {
"name": "s3_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"client_url": {
"name": "client_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"is_public": {
"name": "is_public",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"status": {
"name": "status",
"type": "resource_upload",
"primaryKey": false,
"notNull": true,
"default": "'started'"
},
"upload_machine_id": {
"name": "upload_machine_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"upload_type": {
"name": "upload_type",
"type": "model_upload_type",
"primaryKey": false,
"notNull": true
},
"build_log": {
"name": "build_log",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"checkpoints_user_id_users_id_fk": {
"name": "checkpoints_user_id_users_id_fk",
"tableFrom": "checkpoints",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"checkpoints_checkpoint_volume_id_workflow_runs_id_fk": {
"name": "checkpoints_checkpoint_volume_id_workflow_runs_id_fk",
"tableFrom": "checkpoints",
"tableTo": "workflow_runs",
"columnsFrom": [
"checkpoint_volume_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"checkpoint_volume": {
"name": "checkpoint_volume",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"volume_name": {
"name": "volume_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
"foreignKeys": {
"checkpoint_volume_user_id_users_id_fk": {
"name": "checkpoint_volume_user_id_users_id_fk",
"tableFrom": "checkpoint_volume",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
@@ -129,12 +367,6 @@
"primaryKey": false,
"notNull": true
},
"share_slug": {
"name": "share_slug",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
@@ -224,15 +456,7 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"deployments_share_slug_unique": {
"name": "deployments_share_slug_unique",
"nullsNotDistinct": false,
"columns": [
"share_slug"
]
}
}
"uniqueConstraints": {}
},
"machines": {
"name": "machines",
@@ -520,12 +744,6 @@
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
@@ -746,6 +964,22 @@
"comfy-deploy-serverless": "comfy-deploy-serverless"
}
},
"model_upload_type": {
"name": "model_upload_type",
"values": {
"civitai": "civitai",
"huggingface": "huggingface",
"other": "other"
}
},
"resource_upload": {
"name": "resource_upload",
"values": {
"started": "started",
"error": "error",
"succeded": "succeded"
}
},
"workflow_run_origin": {
"name": "workflow_run_origin",
"values": {
-824
View File
@@ -1,824 +0,0 @@
{
"id": "97662b25-3992-4859-9bdc-560e2a70daea",
"prevId": "1425ee00-66fb-4541-8da7-19b217944545",
"version": "5",
"dialect": "pg",
"tables": {
"api_keys": {
"name": "api_keys",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"revoked": {
"name": "revoked",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"api_keys_user_id_users_id_fk": {
"name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"api_keys_key_unique": {
"name": "api_keys_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
}
}
},
"auth_requests": {
"name": "auth_requests",
"schema": "comfyui_deploy",
"columns": {
"request_id": {
"name": "request_id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"api_hash": {
"name": "api_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"share_slug": {
"name": "share_slug",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"showcase_media": {
"name": "showcase_media",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"environment": {
"name": "environment",
"type": "deployment_environment",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"deployments_user_id_users_id_fk": {
"name": "deployments_user_id_users_id_fk",
"tableFrom": "deployments",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_workflow_version_id_workflow_versions_id_fk": {
"name": "deployments_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "deployments",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"deployments_workflow_id_workflows_id_fk": {
"name": "deployments_workflow_id_workflows_id_fk",
"tableFrom": "deployments",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_machine_id_machines_id_fk": {
"name": "deployments_machine_id_machines_id_fk",
"tableFrom": "deployments",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"deployments_share_slug_unique": {
"name": "deployments_share_slug_unique",
"nullsNotDistinct": false,
"columns": [
"share_slug"
]
}
}
},
"machines": {
"name": "machines",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endpoint": {
"name": "endpoint",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"auth_token": {
"name": "auth_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "machine_type",
"primaryKey": false,
"notNull": true,
"default": "'classic'"
},
"status": {
"name": "status",
"type": "machine_status",
"primaryKey": false,
"notNull": true,
"default": "'ready'"
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"models": {
"name": "models",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gpu": {
"name": "gpu",
"type": "machine_gpu",
"primaryKey": false,
"notNull": false
},
"build_machine_instance_id": {
"name": "build_machine_instance_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"build_log": {
"name": "build_log",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"machines_user_id_users_id_fk": {
"name": "machines_user_id_users_id_fk",
"tableFrom": "machines",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_run_outputs": {
"name": "workflow_run_outputs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_run_outputs_run_id_workflow_runs_id_fk": {
"name": "workflow_run_outputs_run_id_workflow_runs_id_fk",
"tableFrom": "workflow_run_outputs",
"tableTo": "workflow_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_runs": {
"name": "workflow_runs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"workflow_inputs": {
"name": "workflow_inputs",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "workflow_run_origin",
"primaryKey": false,
"notNull": true,
"default": "'api'"
},
"status": {
"name": "status",
"type": "workflow_run_status",
"primaryKey": false,
"notNull": true,
"default": "'not-started'"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"workflow_runs_workflow_version_id_workflow_versions_id_fk": {
"name": "workflow_runs_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"workflow_runs_workflow_id_workflows_id_fk": {
"name": "workflow_runs_workflow_id_workflows_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"workflow_runs_machine_id_machines_id_fk": {
"name": "workflow_runs_machine_id_machines_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflows": {
"name": "workflows",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflows_user_id_users_id_fk": {
"name": "workflows_user_id_users_id_fk",
"tableFrom": "workflows",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_versions": {
"name": "workflow_versions",
"schema": "comfyui_deploy",
"columns": {
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow": {
"name": "workflow",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_api": {
"name": "workflow_api",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_versions_workflow_id_workflows_id_fk": {
"name": "workflow_versions_workflow_id_workflows_id_fk",
"tableFrom": "workflow_versions",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"deployment_environment": {
"name": "deployment_environment",
"values": {
"staging": "staging",
"production": "production",
"public-share": "public-share"
}
},
"machine_gpu": {
"name": "machine_gpu",
"values": {
"T4": "T4",
"A10G": "A10G",
"A100": "A100"
}
},
"machine_status": {
"name": "machine_status",
"values": {
"ready": "ready",
"building": "building",
"error": "error"
}
},
"machine_type": {
"name": "machine_type",
"values": {
"classic": "classic",
"runpod-serverless": "runpod-serverless",
"modal-serverless": "modal-serverless",
"comfy-deploy-serverless": "comfy-deploy-serverless"
}
},
"workflow_run_origin": {
"name": "workflow_run_origin",
"values": {
"manual": "manual",
"api": "api",
"public-share": "public-share"
}
},
"workflow_run_status": {
"name": "workflow_run_status",
"values": {
"not-started": "not-started",
"running": "running",
"uploading": "uploading",
"success": "success",
"failed": "failed"
}
}
},
"schemas": {
"comfyui_deploy": "comfyui_deploy"
},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}
-830
View File
@@ -1,830 +0,0 @@
{
"id": "8d654f92-7f7e-420f-bbd3-73b6b27adf35",
"prevId": "97662b25-3992-4859-9bdc-560e2a70daea",
"version": "5",
"dialect": "pg",
"tables": {
"api_keys": {
"name": "api_keys",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"revoked": {
"name": "revoked",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"api_keys_user_id_users_id_fk": {
"name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"api_keys_key_unique": {
"name": "api_keys_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
}
}
},
"auth_requests": {
"name": "auth_requests",
"schema": "comfyui_deploy",
"columns": {
"request_id": {
"name": "request_id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"api_hash": {
"name": "api_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"expired_date": {
"name": "expired_date",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"share_slug": {
"name": "share_slug",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"showcase_media": {
"name": "showcase_media",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"environment": {
"name": "environment",
"type": "deployment_environment",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"deployments_user_id_users_id_fk": {
"name": "deployments_user_id_users_id_fk",
"tableFrom": "deployments",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_workflow_version_id_workflow_versions_id_fk": {
"name": "deployments_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "deployments",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"deployments_workflow_id_workflows_id_fk": {
"name": "deployments_workflow_id_workflows_id_fk",
"tableFrom": "deployments",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_machine_id_machines_id_fk": {
"name": "deployments_machine_id_machines_id_fk",
"tableFrom": "deployments",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"deployments_share_slug_unique": {
"name": "deployments_share_slug_unique",
"nullsNotDistinct": false,
"columns": [
"share_slug"
]
}
}
},
"machines": {
"name": "machines",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endpoint": {
"name": "endpoint",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"auth_token": {
"name": "auth_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "machine_type",
"primaryKey": false,
"notNull": true,
"default": "'classic'"
},
"status": {
"name": "status",
"type": "machine_status",
"primaryKey": false,
"notNull": true,
"default": "'ready'"
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"models": {
"name": "models",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gpu": {
"name": "gpu",
"type": "machine_gpu",
"primaryKey": false,
"notNull": false
},
"build_machine_instance_id": {
"name": "build_machine_instance_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"build_log": {
"name": "build_log",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"machines_user_id_users_id_fk": {
"name": "machines_user_id_users_id_fk",
"tableFrom": "machines",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_run_outputs": {
"name": "workflow_run_outputs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_run_outputs_run_id_workflow_runs_id_fk": {
"name": "workflow_run_outputs_run_id_workflow_runs_id_fk",
"tableFrom": "workflow_run_outputs",
"tableTo": "workflow_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_runs": {
"name": "workflow_runs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"workflow_inputs": {
"name": "workflow_inputs",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "workflow_run_origin",
"primaryKey": false,
"notNull": true,
"default": "'api'"
},
"status": {
"name": "status",
"type": "workflow_run_status",
"primaryKey": false,
"notNull": true,
"default": "'not-started'"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"workflow_runs_workflow_version_id_workflow_versions_id_fk": {
"name": "workflow_runs_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"workflow_runs_workflow_id_workflows_id_fk": {
"name": "workflow_runs_workflow_id_workflows_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"workflow_runs_machine_id_machines_id_fk": {
"name": "workflow_runs_machine_id_machines_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflows": {
"name": "workflows",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflows_user_id_users_id_fk": {
"name": "workflows_user_id_users_id_fk",
"tableFrom": "workflows",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_versions": {
"name": "workflow_versions",
"schema": "comfyui_deploy",
"columns": {
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow": {
"name": "workflow",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_api": {
"name": "workflow_api",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_versions_workflow_id_workflows_id_fk": {
"name": "workflow_versions_workflow_id_workflows_id_fk",
"tableFrom": "workflow_versions",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"deployment_environment": {
"name": "deployment_environment",
"values": {
"staging": "staging",
"production": "production",
"public-share": "public-share"
}
},
"machine_gpu": {
"name": "machine_gpu",
"values": {
"T4": "T4",
"A10G": "A10G",
"A100": "A100"
}
},
"machine_status": {
"name": "machine_status",
"values": {
"ready": "ready",
"building": "building",
"error": "error"
}
},
"machine_type": {
"name": "machine_type",
"values": {
"classic": "classic",
"runpod-serverless": "runpod-serverless",
"modal-serverless": "modal-serverless",
"comfy-deploy-serverless": "comfy-deploy-serverless"
}
},
"workflow_run_origin": {
"name": "workflow_run_origin",
"values": {
"manual": "manual",
"api": "api",
"public-share": "public-share"
}
},
"workflow_run_status": {
"name": "workflow_run_status",
"values": {
"not-started": "not-started",
"running": "running",
"uploading": "uploading",
"success": "success",
"failed": "failed"
}
}
},
"schemas": {
"comfyui_deploy": "comfyui_deploy"
},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}
+4 -18
View File
@@ -222,29 +222,15 @@
{
"idx": 31,
"version": "5",
"when": 1705763980972,
"tag": "0031_fast_lyja",
"when": 1705975916818,
"tag": "0031_safe_multiple_man",
"breakpoints": true
},
{
"idx": 32,
"version": "5",
"when": 1705806921697,
"tag": "0032_shallow_vermin",
"breakpoints": true
},
{
"idx": 33,
"version": "5",
"when": 1705853314500,
"tag": "0033_awesome_human_fly",
"breakpoints": true
},
{
"idx": 34,
"version": "5",
"when": 1705902960991,
"tag": "0034_even_lady_ursula",
"when": 1705979098372,
"tag": "0032_material_wallflower",
"breakpoints": true
}
]
+7 -12
View File
@@ -12,31 +12,26 @@ let sslMode: string | boolean = process.env.SSL || "require";
if (sslMode === "false") sslMode = false;
let connectionString = process.env.POSTGRES_URL!;
const isDevContainer = process.env.REMOTE_CONTAINERS !== undefined;
if (isDevContainer)
connectionString = connectionString.replace(
"localhost",
"host.docker.internal",
);
console.log(migrationsFolderName, sslMode);
const connectionString = process.env.POSTGRES_URL!;
console.log(connectionString);
const sql = postgres(connectionString, { max: 1, ssl: sslMode as any });
const db = drizzle(sql, {
logger: true,
});
let retries = 5;
while (retries) {
while(retries) {
try {
await sql`SELECT NOW()`;
console.log("Database is live");
console.log('Database is live');
break;
} catch (error) {
console.error("Database is not live yet", error);
console.error('Database is not live yet', error);
retries -= 1;
console.log(`Retries left: ${retries}`);
await new Promise((res) => setTimeout(res, 1000));
await new Promise(res => setTimeout(res, 1000));
}
}
+11 -4
View File
@@ -12,8 +12,7 @@
"migrate-production": "bun run migrate.mts",
"migrate-local": "SSL=false LOCAL=true bun run migrate.mts",
"db-up": "docker-compose up",
"db-dev": "bun run db-up && bun run migrate-local",
"lint:fix": "bunx @biomejs/biome lint --apply ./src"
"db-dev": "bun run db-up && bun run migrate-local"
},
"dependencies": {
"@algolia/autocomplete-core": "^1.13.0",
@@ -72,7 +71,6 @@
"mdx-annotations": "^0.1.4",
"million": "latest",
"mitata": "^0.1.6",
"ms": "^2.1.3",
"nanoid": "^5.0.4",
"next": "14.1",
"next-plausible": "^3.12.0",
@@ -107,17 +105,26 @@
"zustand": "^4.4.7"
},
"devDependencies": {
"@biomejs/biome": "1.5.2",
"@trivago/prettier-plugin-sort-imports": "4.1.1",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"autoprefixer": "^10.0.1",
"concurrently": "^8.2.2",
"dotenv": "^16.3.1",
"drizzle-kit": "^0.20.6",
"eslint": "8.34.0",
"eslint-config-next": "^14.0.4",
"eslint-config-prettier": "^8.6.0",
"eslint-config-turbo": "latest",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-unused-imports": "^3.0.0",
"postcss": "^8",
"postgres": "^3.4.3",
"prettier": "2.8.6",
"prettier-plugin-tailwindcss": "0.2.5",
"sharp": "^0.33.1",
"tailwindcss": "^3.3.0",
"typescript": "^5"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

+11 -42
View File
@@ -1,3 +1,4 @@
import { app } from "../../../../routes/app";
import { registerCreateRunRoute } from "@/routes/registerCreateRunRoute";
import { registerGetOutputRoute } from "@/routes/registerGetOutputRoute";
import { registerUploadRoute } from "@/routes/registerUploadRoute";
@@ -5,11 +6,7 @@ import { isKeyRevoked } from "@/server/curdApiKeys";
import { parseJWT } from "@/server/parseJWT";
import type { Context, Next } from "hono";
import { handle } from "hono/vercel";
import { app } from "../../../../routes/app";
import { registerWorkflowUploadRoute } from "@/routes/registerWorkflowUploadRoute";
import { registerGetAuthResponse } from "@/routes/registerGetAuthResponse";
import { registerGetWorkflowRoute } from "@/routes/registerGetWorkflow";
import { cors } from "hono/cors";
export const dynamic = "force-dynamic";
export const maxDuration = 300; // 5 minutes
@@ -19,24 +16,14 @@ declare module "hono" {
}
}
async function checkAuth(c: Context, next: Next, headers?: HeadersInit) {
async function checkAuth(c: Context, next: Next) {
const token = c.req.raw.headers.get("Authorization")?.split(" ")?.[1]; // Assuming token is sent as "Bearer your_token"
const userData = token ? parseJWT(token) : undefined;
if (!userData || token === undefined) {
return c.text("Invalid or expired token", {
status: 401,
headers: headers,
});
}
// If the key has expiration, this is a temporary key and not in our db, so we can skip checking
if (userData.exp === undefined) {
return c.text("Invalid or expired token", 401);
} else {
const revokedKey = await isKeyRevoked(token);
if (revokedKey)
return c.text("Revoked token", {
status: 401,
headers: headers,
});
if (revokedKey) return c.text("Revoked token", 401);
}
c.set("apiKeyTokenData", userData);
@@ -44,35 +31,18 @@ async function checkAuth(c: Context, next: Next, headers?: HeadersInit) {
await next();
}
app.use("/run", checkAuth);
app.use("/upload-url", checkAuth);
const corsHandler = cors({
origin: "*",
allowHeaders: ["Authorization", "Content-Type"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
app.use("/run", async (c, next) => {
return checkAuth(c, next);
});
// CORS Check
app.use("/workflow", corsHandler, checkAuth);
app.use("/workflow-version/*", corsHandler, checkAuth);
app.use("/upload-url", async (c, next) => {
return checkAuth(c, next);
});
// create run endpoint
registerCreateRunRoute(app);
registerGetOutputRoute(app);
// file upload endpoint
registerUploadRoute(app);
// Anon
registerGetAuthResponse(app);
registerWorkflowUploadRoute(app);
registerGetWorkflowRoute(app);
// The OpenAPI documentation will be available at /doc
app.doc("/doc", {
openapi: "3.0.0",
@@ -99,4 +69,3 @@ const handler = handle(app);
export const GET = handler;
export const POST = handler;
export const OPTIONS = handler;
+3 -4
View File
@@ -1,12 +1,11 @@
import { parseDataSafe } from "../../../../lib/parseDataSafe";
import { handleResourceUpload } from "@/server/resource";
import { NextResponse } from "next/server";
import { z } from "zod";
import { parseDataSafe } from "../../../../lib/parseDataSafe";
const Request = z.object({
file_name: z.string(),
run_id: z.string(),
type: z.string(),
});
@@ -30,7 +29,7 @@ export async function GET(request: Request) {
{
url: uploadUrl,
},
{ status: 200 },
{ status: 200 }
);
} catch (error: unknown) {
const errorMessage =
@@ -39,7 +38,7 @@ export async function GET(request: Request) {
{
error: errorMessage,
},
{ status: 500 },
{ status: 500 }
);
}
}
+63 -21
View File
@@ -1,14 +1,17 @@
import { snapshotType, workflowAPIType, workflowType } from "@/db/schema";
import { createNewWorkflow } from "../../../../server/createNewWorkflow";
import { parseJWT } from "../../../../server/parseJWT";
import { db } from "@/db/db";
import {
snapshotType,
workflowAPIType,
workflowTable,
workflowType,
workflowVersionTable,
} from "@/db/schema";
import { parseDataSafe } from "@/lib/parseDataSafe";
import { eq, sql } from "drizzle-orm";
import { NextResponse } from "next/server";
import { z } from "zod";
import {
createNewWorkflow,
createNewWorkflowVersion,
} from "../../../../server/createNewWorkflow";
import { parseJWT } from "../../../../server/parseJWT";
// This is will be deprecated
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
@@ -52,7 +55,7 @@ export async function POST(request: Request) {
const [data, error] = await parseDataSafe(
UploadRequest,
request,
corsHeaders,
corsHeaders
);
if (!data || error) return error;
@@ -72,7 +75,7 @@ export async function POST(request: Request) {
// Case 1 new workflow
try {
if ((!workflow_id || workflow_id.length === 0) && workflow_name) {
if ((!workflow_id || workflow_id.length == 0) && workflow_name) {
// Create a new parent workflow
const { workflow_id: _workflow_id, version: _version } =
await createNewWorkflow({
@@ -88,17 +91,56 @@ export async function POST(request: Request) {
workflow_id = _workflow_id;
version = _version;
// const workflow_parent = await db
// .insert(workflowTable)
// .values({
// user_id,
// name: workflow_name,
// org_id: org_id,
// })
// .returning();
// workflow_id = workflow_parent[0].id;
// // Create a new version
// const data = await db
// .insert(workflowVersionTable)
// .values({
// workflow_id: workflow_id,
// workflow,
// workflow_api,
// version: 1,
// snapshot: snapshot,
// })
// .returning();
// version = data[0].version;
} else if (workflow_id) {
// Case 2 update workflow
const { version: _version } = await createNewWorkflowVersion({
workflow_id: workflow_id,
workflowData: {
workflow,
const data = await db
.insert(workflowVersionTable)
.values({
workflow_id,
workflow: workflow,
workflow_api,
snapshot,
},
});
version = _version;
// version: sql`${workflowVersionTable.version} + 1`,
snapshot: snapshot,
version: sql`(
SELECT COALESCE(MAX(version), 0) + 1
FROM ${workflowVersionTable}
WHERE workflow_id = ${workflow_id}
)`,
})
.returning();
version = data[0].version;
await db
.update(workflowTable)
.set({
updated_at: new Date(),
})
.where(eq(workflowTable.id, workflow_id))
.returning();
} else {
return NextResponse.json(
{
@@ -108,7 +150,7 @@ export async function POST(request: Request) {
status: 500,
statusText: "Invalid request",
headers: corsHeaders,
},
}
);
}
} catch (error: any) {
@@ -120,7 +162,7 @@ export async function POST(request: Request) {
status: 500,
statusText: "Invalid request",
headers: corsHeaders,
},
}
);
}
@@ -132,6 +174,6 @@ export async function POST(request: Request) {
{
status: 200,
headers: corsHeaders,
},
}
);
}
@@ -0,0 +1,50 @@
import { parseDataSafe } from "../../../../lib/parseDataSafe";
import { db } from "@/db/db";
import { checkpointTable, machinesTable } from "@/db/schema";
import { eq } from "drizzle-orm";
import { NextResponse } from "next/server";
import { z } from "zod";
const Request = z.object({
machine_id: z.string(),
endpoint: z.string().optional(),
build_log: z.string().optional(),
});
export async function POST(request: Request) {
const [data, error] = await parseDataSafe(Request, request);
if (!data || error) return error;
// console.log(data);
const { machine_id, endpoint, build_log } = data;
if (endpoint) {
await db
.update(checkpointTable)
.set({
// status: "ready",
// endpoint: endpoint,
// build_log: build_log,
})
.where(eq(machinesTable.id, machine_id));
} else {
// console.log(data);
await db
.update(machinesTable)
.set({
// status: "error",
// build_log: build_log,
})
.where(eq(machinesTable.id, machine_id));
}
return NextResponse.json(
{
message: "success",
},
{
status: 200,
}
);
}
@@ -1,54 +0,0 @@
import { ButtonAction } from "@/components/ButtonActionLoader";
import { Button } from "@/components/ui/button";
import { createAuthRequest } from "@/server/curdApiKeys";
import { auth } from "@clerk/nextjs";
import { redirect } from "next/navigation";
import { getOrgOrUserDisplayName } from "../../../../server/getOrgOrUserDisplayName";
import { db } from "@/db/db";
import { eq } from "drizzle-orm";
import { authRequestsTable } from "@/db/schema";
export default async function Home({
params,
}: {
params: { request_id: string };
}) {
const { userId, orgId } = await auth();
if (!userId) redirect("/");
if (!params.request_id)
return (
<div className="h-full w-full flex flex-col gap-2 items-center justify-center">
No valid request_id
</div>
);
const existingResult = await db.query.authRequestsTable.findFirst({
where: eq(authRequestsTable.request_id, params.request_id),
});
if (existingResult?.api_hash) {
return (
<div className="h-full w-full flex flex-col gap-2 items-center justify-center">
Request already consumed.
</div>
);
}
const userName = await getOrgOrUserDisplayName(orgId, userId);
return (
<div className="h-full w-full flex flex-col gap-2 items-center justify-center">
<div className="text-lg">Grant API Access to {userName}</div>
<Button asChild>
<ButtonAction
routerAction="do-nothing"
action={createAuthRequest.bind(null, params.request_id)}
>
Grant Access
</ButtonAction>
</Button>
</div>
);
}
-90
View File
@@ -1,90 +0,0 @@
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>
);
}
+81 -79
View File
@@ -2,11 +2,11 @@ import { ButtonActionMenu } from "@/components/ButtonActionLoader";
import { RunWorkflowInline } from "@/components/RunWorkflowInline";
import { PublicRunOutputs } from "@/components/VersionSelect";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { db } from "@/db/db";
import { usersTable } from "@/db/schema";
@@ -14,9 +14,9 @@ import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
import { getRelativeTime } from "@/lib/getRelativeTime";
import { setInitialUserData } from "@/lib/setInitialUserData";
import {
cloneMachine,
cloneWorkflow,
findSharedDeployment,
cloneMachine,
cloneWorkflow,
findSharedDeployment,
} from "@/server/curdDeploments";
import { auth, clerkClient } from "@clerk/nextjs/server";
import { eq } from "drizzle-orm";
@@ -25,87 +25,89 @@ import { redirect } from "next/navigation";
export const maxDuration = 300; // 5 minutes
export default async function Page({
params,
params,
}: {
params: { share_id: string };
params: { share_id: string };
}) {
const { userId } = await auth();
const { userId } = await auth();
// If there is user, check if the user data is present
if (userId) {
const user = await db.query.usersTable.findFirst({
where: eq(usersTable.id, userId),
});
// If there is user, check if the user data is present
if (userId) {
const user = await db.query.usersTable.findFirst({
where: eq(usersTable.id, userId),
});
if (!user) {
await setInitialUserData(userId);
}
}
if (!user) {
await setInitialUserData(userId);
}
}
const sharedDeployment = await findSharedDeployment(params.share_id);
const sharedDeployment = await findSharedDeployment(params.share_id);
if (!sharedDeployment) return redirect("/");
if (!sharedDeployment) return redirect("/");
const userName = sharedDeployment.workflow.org_id
? await clerkClient.organizations
.getOrganization({
organizationId: sharedDeployment.workflow.org_id,
})
.then((x) => x.name)
: sharedDeployment.user.name;
const userName = sharedDeployment.workflow.org_id
? await clerkClient.organizations
.getOrganization({
organizationId: sharedDeployment.workflow.org_id,
})
.then((x) => x.name)
: sharedDeployment.user.name;
const inputs = getInputsFromWorkflow(sharedDeployment.version);
const inputs = getInputsFromWorkflow(sharedDeployment.version);
return (
<div className="mt-4 w-full grid grid-rows-[1fr,1fr] lg:grid-cols-[minmax(auto,500px),1fr] gap-4 max-h-[calc(100dvh-100px)]">
<Card className="w-full h-fit mt-4">
<CardHeader>
<CardTitle className="flex justify-between items-center">
<div>
{userName}
{" / "}
{sharedDeployment.workflow.name}
</div>
return (
<div className="mt-4 w-full grid grid-rows-[1fr,1fr] lg:grid-cols-[minmax(auto,500px),1fr] gap-4 max-h-[calc(100dvh-100px)]">
<Card className="w-full h-fit mt-4">
<CardHeader>
<CardTitle className="flex justify-between items-center">
<div>
{userName}
{" / "}
{sharedDeployment.workflow.name}
</div>
<ButtonActionMenu
title="Clone"
actions={[
{
title: "Workflow",
action: cloneWorkflow.bind(null, sharedDeployment.id),
},
{
title: "Machine",
action: cloneMachine.bind(null, sharedDeployment.id),
},
]}
/>
</CardTitle>
<CardDescription suppressHydrationWarning={true}>
{getRelativeTime(sharedDeployment?.updated_at)}
</CardDescription>
</CardHeader>
<ButtonActionMenu
title="Clone"
actions={[
{
title: "Workflow",
action: cloneWorkflow.bind(null, sharedDeployment.id),
},
{
title: "Machine",
action: cloneMachine.bind(null, sharedDeployment.id),
},
]}
/>
</CardTitle>
<CardDescription suppressHydrationWarning={true}>
{getRelativeTime(sharedDeployment?.updated_at)}
</CardDescription>
</CardHeader>
<CardContent>
<div>
{sharedDeployment?.description && sharedDeployment?.description}
</div>
<RunWorkflowInline
inputs={inputs}
machine_id={sharedDeployment.machine_id}
workflow_version_id={sharedDeployment.workflow_version_id}
/>
</CardContent>
</Card>
<Card className="w-full h-fit mt-4">
<CardHeader>
<CardDescription>Run outputs</CardDescription>
</CardHeader>
<CardContent>
<div>
{sharedDeployment?.description && (
<>{sharedDeployment?.description}</>
)}
</div>
<RunWorkflowInline
inputs={inputs}
machine_id={sharedDeployment.machine_id}
workflow_version_id={sharedDeployment.workflow_version_id}
/>
</CardContent>
</Card>
<Card className="w-full h-fit mt-4">
<CardHeader>
<CardDescription>Run outputs</CardDescription>
</CardHeader>
<CardContent>
<PublicRunOutputs preview={sharedDeployment.showcase_media} />
</CardContent>
</Card>
</div>
);
<CardContent>
<PublicRunOutputs preview={sharedDeployment.showcase_media} />
</CardContent>
</Card>
</div>
);
}
+35
View File
@@ -0,0 +1,35 @@
import { setInitialUserData } from "../../../lib/setInitialUserData";
import { auth } from "@clerk/nextjs";
import { clerkClient } from "@clerk/nextjs/server";
import { CheckpointList } from "@/components/CheckpointList"
import { getAllUserCheckpoints } from "@/server/getAllUserCheckpoints";
export default function Page() {
return <CheckpointListServer />;
}
async function CheckpointListServer() {
const { userId } = auth();
if (!userId) {
return <div>No auth</div>;
}
const user = await clerkClient.users.getUser(userId);
if (!user) {
await setInitialUserData(userId);
}
const checkpoints = await getAllUserCheckpoints()
if (!checkpoints) {
return <div>No checkpoints found</div>;
}
return (
<div className="w-full">
<CheckpointList data={checkpoints}/>
</div>
);
}
@@ -1,11 +1,10 @@
import { CreateShareButton } from "@/components/CreateShareButton";
import { MachinesWSMain } from "@/components/MachinesWS";
import { VersionDetails } from "@/components/VersionDetails";
import {
CopyWorkflowVersion,
CreateDeploymentButton,
CreateShareButton,
MachineSelect,
OpenEditButton,
RunWorkflowButton,
VersionSelect,
ViewWorkflowDetailsButton,
@@ -49,7 +48,6 @@ export default async function Page({
<CreateShareButton workflow={workflow} machines={machines} />
<CopyWorkflowVersion workflow={workflow} />
<ViewWorkflowDetailsButton workflow={workflow} />
<OpenEditButton workflow={workflow} machines={machines} />
</div>
<VersionDetails workflow={workflow} />
+1 -1
View File
@@ -1,5 +1,5 @@
import { setInitialUserData } from "../../../lib/setInitialUserData";
import { getAllUserWorkflow } from "../../../server/crudWorkflow";
import { getAllUserWorkflow } from "../../../server/getAllUserWorkflow";
import { WorkflowList } from "@/components/WorkflowList";
import { db } from "@/db/db";
import { usersTable } from "@/db/schema";
+1 -6
View File
@@ -17,11 +17,9 @@ import { useState } from "react";
export function ButtonAction({
action,
children,
routerAction = "back",
...rest
}: {
action: () => Promise<any>;
routerAction?: "refresh" | "back" | "do-nothing";
children: React.ReactNode;
}) {
const [pending, setPending] = useState(false);
@@ -36,10 +34,7 @@ export function ButtonAction({
await callServerPromise(action());
setPending(false);
if (routerAction === "back") {
router.back();
router.refresh();
} else if (routerAction === "refresh") router.refresh();
router.refresh();
}}
{...rest}
>
+315
View File
@@ -0,0 +1,315 @@
"use client";
import { getRelativeTime } from "../lib/getRelativeTime";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { InsertModal, UpdateModal } from "./InsertModal";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { getAllUserCheckpoints } from "@/server/getAllUserCheckpoints";
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from "@tanstack/react-table";
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { insertCivitaiCheckpointSchema } from "@/db/schema";
import { addCivitaiCheckpoint } from "@/server/curdCheckpoint";
import { addCivitaiCheckpointSchema } from "@/server/addCheckpointSchema";
export type CheckpointItemList = NonNullable<
Awaited<ReturnType<typeof getAllUserCheckpoints>>
>[0];
export const columns: ColumnDef<CheckpointItemList>[] = [
{
accessorKey: "id",
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: ({ column }) => {
return (
<button
className="flex items-center hover:underline"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => {
const checkpoint = row.original;
return (
<a
className="hover:underline flex gap-2"
href={`/storage/${checkpoint.id}`} // TODO
>
<span className="truncate max-w-[200px]">{row.original.model_name}</span>
<Badge variant="default">{}</Badge>
{checkpoint.is_public
? <Badge variant="success">Public</Badge>
: <Badge variant="teal">Private</Badge>}
</a>
);
},
},
{
accessorKey: "creator",
header: ({ column }) => {
return (
<button
className="flex items-center hover:underline"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Creator
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => {
// return <Badge variant="cyan">{row?.original?.user?.name ? row.original.user.name : "Public"}</Badge>;
},
},
{
accessorKey: "date",
sortingFn: "datetime",
enableSorting: true,
header: ({ column }) => {
return (
<button
className="w-full flex items-center justify-end hover:underline truncate"
// variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Update Date
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => (
<div className="w-full capitalize text-right truncate">
{getRelativeTime(row.original.updated_at)}
</div>
),
},
// {
// id: "actions",
// enableHiding: false,
// cell: ({ row }) => {
// const checkpoint = row.original;
//
// return (
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button variant="ghost" className="h-8 w-8 p-0">
// <span className="sr-only">Open menu</span>
// <MoreHorizontal className="h-4 w-4" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent align="end">
// <DropdownMenuLabel>Actions</DropdownMenuLabel>
// <DropdownMenuItem
// className="text-destructive"
// onClick={() => {
// deleteWorkflow(checkpoint.id);
// }}
// >
// Delete Workflow
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// );
// },
// },
];
export function CheckpointList({ data }: { data: CheckpointItemList[] }) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] = React.useState<
VisibilityState
>({});
const [rowSelection, setRowSelection] = React.useState({});
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
return (
<div className="grid grid-rows-[auto,1fr,auto] h-full">
<div className="flex flex-row w-full items-center py-4">
<Input
placeholder="Filter workflows..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("name")?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
<div className="ml-auto flex gap-2">
<InsertModal
dialogClassName="sm:max-w-[600px]"
disabled={
false
// TODO: limitations based on plan
}
tooltip={"Add models using their civitai url!"}
title="Civitai Checkpoint"
description="Pick a model from civitai"
serverAction={addCivitaiCheckpoint}
formSchema={addCivitaiCheckpointSchema}
fieldConfig={{
civitai_url: {
fieldType: "fallback",
// fieldType: "fallback",
inputProps: { required: true },
description: (
<>
Pick a checkpoint from{" "}
<a
href="https://www.civitai.com/models"
target="_blank"
className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"
>
civitai.com
</a>{" "}
and place it's url here
</>
),
},
}}
/>
</div>
</div>
<ScrollArea className="h-full w-full rounded-md border">
<Table>
<TableHeader className="bg-background top-0 sticky">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length
? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
)
: (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</ScrollArea>
<div className="flex flex-row items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}
-66
View File
@@ -1,66 +0,0 @@
"use client";
import { LoadingIcon } from "@/components/LoadingIcon";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { createDeployments } from "@/server/curdDeploments";
import type { getMachines } from "@/server/curdMachine";
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
import { Share } from "lucide-react";
import { parseAsInteger, useQueryState } from "next-usequerystate";
import { useState } from "react";
import { useSelectedMachine } from "./VersionSelect";
import { callServerPromise } from "./callServerPromise";
export function CreateShareButton({
workflow,
machines,
}: {
workflow: Awaited<ReturnType<typeof findFirstTableWithVersion>>;
machines: Awaited<ReturnType<typeof getMachines>>;
}) {
const [version] = useQueryState("version", {
defaultValue: workflow?.versions[0].version ?? 1,
...parseAsInteger,
});
const [machine] = useSelectedMachine(machines);
const [isLoading, setIsLoading] = useState(false);
const workflow_version_id = workflow?.versions.find(
(x) => x.version == version,
)?.id;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="gap-2" disabled={isLoading} variant="outline">
Share {isLoading ? <LoadingIcon /> : <Share size={14} />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem
onClick={async () => {
if (!workflow_version_id) return;
setIsLoading(true);
await callServerPromise(
createDeployments(
workflow.id,
workflow_version_id,
machine,
"public-share",
),
);
setIsLoading(false);
}}
>
Public
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
+10 -11
View File
@@ -1,3 +1,4 @@
import { DeploymentRow, SharePageDeploymentRow } from "./DeploymentRow";
import { CodeBlock } from "@/components/CodeBlock";
import {
Dialog,
@@ -12,7 +13,6 @@ import { TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
import type { findAllDeployments } from "@/server/findAllRuns";
import { DeploymentRow, SharePageDeploymentRow } from "./DeploymentRow";
const curlTemplate = `
curl --request POST \
@@ -91,7 +91,7 @@ export function DeploymentDisplay({
}) {
const workflowInput = getInputsFromWorkflow(deployment.version);
if (deployment.environment === "public-share") {
if (deployment.environment == "public-share") {
return <SharePageDeploymentRow deployment={deployment} />;
}
@@ -123,7 +123,6 @@ export function DeploymentDisplay({
href="https://github.com/BennyKok/comfyui-deploy-next-example/blob/main/src/lib/comfy-deploy.ts"
className="text-blue-500 hover:underline"
target="_blank"
rel="noreferrer"
>
here
</a>
@@ -136,7 +135,7 @@ export function DeploymentDisplay({
: jsClientSetupTemplate,
deployment,
domain,
workflowInput,
workflowInput
)}
/>
Create a run via deployment id
@@ -148,7 +147,7 @@ export function DeploymentDisplay({
: jsClientCreateRunNoInputsTemplate,
deployment,
domain,
workflowInput,
workflowInput
)}
/>
Check the status of the run, and retrieve the outputs
@@ -157,7 +156,7 @@ export function DeploymentDisplay({
code={formatCode(
clientTemplate_checkStatus,
deployment,
domain,
domain
)}
/>
</TabsContent>
@@ -195,7 +194,7 @@ function formatCode(
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0],
domain: string,
inputs?: ReturnType<typeof getInputsFromWorkflow>,
inputsTabs?: number,
inputsTabs?: number
) {
if (inputs && inputs.length > 0) {
codeTemplate = codeTemplate.replace(
@@ -204,20 +203,20 @@ function formatCode(
Object.fromEntries(
inputs.map((x) => {
return [x?.input_id, ""];
}),
})
),
null,
2,
2
)
.split("\n")
.map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line
.join("\n")}`,
.join("\n")}`
);
} else {
codeTemplate = codeTemplate.replace(
`
inputs: {}`,
"",
""
);
}
return codeTemplate
+44 -46
View File
@@ -6,57 +6,55 @@ import type { findAllDeployments } from "@/server/findAllRuns";
import { useRouter } from "next/navigation";
export function SharePageDeploymentRow({
deployment,
deployment,
}: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) {
const router = useRouter();
return (
<TableRow
className="appearance-none hover:cursor-pointer"
onClick={() => {
if (deployment.environment === "public-share") {
router.push(
`/share/${deployment.share_slug ?? deployment.id}/settings`,
);
}
}}
>
<TableCell className="capitalize truncate">
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</TableRow>
);
const router = useRouter();
return (
<TableRow
className="appearance-none hover:cursor-pointer"
onClick={() => {
if (deployment.environment == "public-share") {
router.push(`/share/${deployment.id}/settings`);
}
}}
>
<TableCell className="capitalize truncate">
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</TableRow>
);
}
export function DeploymentRow({
deployment,
deployment,
}: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) {
return (
<>
<TableCell className="capitalize truncate">
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</>
);
return (
<>
<TableCell className="capitalize truncate">
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</>
);
}
+1 -1
View File
@@ -223,7 +223,7 @@ export const columns: ColumnDef<Machine>[] = [
href={machine.endpoint.replace(
"comfyui-api",
"comfyui-app"
)} rel="noreferrer"
)}
>
Open ComfyUI
</a>
+3 -7
View File
@@ -29,7 +29,6 @@ export function Navbar() {
const { organization } = useOrganization();
const _isDesktop = useMediaQuery("(min-width: 1024px)");
const [isDesktop, setIsDesktop] = useState(true);
const [isSheetOpen, setSheetOpen] = useState(false);
useEffect(() => {
setIsDesktop(_isDesktop);
}, [_isDesktop]);
@@ -37,7 +36,7 @@ export function Navbar() {
<>
<div className="flex flex-row items-center gap-4">
{!isDesktop && (
<Sheet open={isSheetOpen} onOpenChange={(open) => setSheetOpen(open)}>
<Sheet>
<SheetTrigger asChild>
<button className="flex items-center justify-center w-8 h-8 p-2">
<Menu />
@@ -48,10 +47,7 @@ export function Navbar() {
<SheetTitle className="text-start">Comfy Deploy</SheetTitle>
</SheetHeader>
<div className="grid h-full grid-rows-[1fr_auto]">
<NavbarMenu
className=" h-full"
closeSheet={() => setSheetOpen(false)}
/>
<NavbarMenu className=" h-full" />
{/* <OrganizationSwitcher
appearance={{
elements: {
@@ -102,7 +98,7 @@ export function Navbar() {
variant="outline"
className="rounded-full aspect-square p-2"
>
<a target="_blank" href="https://github.com/BennyKok/comfyui-deploy" rel="noreferrer">
<a target="_blank" href="https://github.com/BennyKok/comfyui-deploy">
<Github />
</a>
</Button>
+6 -15
View File
@@ -9,13 +9,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useMediaQuery } from "usehooks-ts";
export function NavbarMenu({
className,
closeSheet,
}: {
className?: string;
closeSheet?: () => void;
}) {
export function NavbarMenu({ className }: { className?: string }) {
const _isDesktop = useMediaQuery("(min-width: 1024px)");
const [isDesktop, setIsDesktop] = useState(true);
useEffect(() => {
@@ -41,9 +35,9 @@ export function NavbarMenu({
path: "/api-keys",
},
{
name: "Examples",
path: "/examples"
}
name: "Storage",
path: "/storage",
},
];
return (
@@ -52,9 +46,9 @@ export function NavbarMenu({
{isDesktop && (
<Tabs
defaultValue={pathname}
className="w-fit flex pointer-events-auto"
className="w-[400px] flex pointer-events-auto"
>
<TabsList className="w-full">
<TabsList className="grid w-full grid-cols-4">
{pages.map((page) => (
<TabsTrigger
key={page.name}
@@ -78,9 +72,6 @@ export function NavbarMenu({
<Link
key={page.name}
href={page.path}
onClick={() => {
if (!!closeSheet) closeSheet();
}}
className="p-2 hover:bg-gray-100/20 hover:underline"
>
{page.name}
+2 -16
View File
@@ -5,20 +5,6 @@ export async function OutputRender(props: {
run_id: 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 (
props.filename.endsWith(".png") ||
props.filename.endsWith(".gif") ||
@@ -26,13 +12,13 @@ export async function OutputRender(props: {
props.filename.endsWith(".jpeg")
) {
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} />;
} else {
const url = await getFileDownloadUrl(
`outputs/runs/${props.run_id}/${props.filename}`,
`outputs/runs/${props.run_id}/${props.filename}`
);
// console.log(url);
+49 -60
View File
@@ -1,74 +1,63 @@
import { LiveStatus } from "./LiveStatus";
import { RunInputs } from "@/components/RunInputs";
import { RunOutputs } from "@/components/RunOutputs";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { TableCell, TableRow } from "@/components/ui/table";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { getDuration, getRelativeTime } from "@/lib/getRelativeTime";
import { type findAllRuns } from "@/server/findAllRuns";
import { Suspense } from "react";
import { LiveStatus } from "./LiveStatus";
export async function RunDisplay({
run,
run,
}: {
run: Awaited<ReturnType<typeof findAllRuns>>[0];
run: Awaited<ReturnType<typeof findAllRuns>>[0];
}) {
return (
<Dialog>
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<TableRow>
<TableCell>{run.number}</TableCell>
<TableCell className="font-medium truncate">
{run.machine?.name}
</TableCell>
<TableCell className="truncate">
{getRelativeTime(run.created_at)}
</TableCell>
<TableCell>{run.version?.version}</TableCell>
<TableCell>
<Badge variant="outline" className="truncate">
{run.origin}
</Badge>
</TableCell>
<TableCell className="truncate">
<Tooltip>
<TooltipTrigger>{getDuration(run.duration)}</TooltipTrigger>
<TooltipContent>
<div>Cold start: {getDuration(run.cold_start_duration)}</div>
<div>Run duration: {getDuration(run.run_duration)}</div>
</TooltipContent>
</Tooltip>
</TableCell>
<LiveStatus run={run} />
</TableRow>
</DialogTrigger>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Run outputs</DialogTitle>
<DialogDescription>
You can view your run&apos;s outputs here
</DialogDescription>
</DialogHeader>
<div className="max-h-96 overflow-y-scroll">
<RunInputs run={run} />
<Suspense>
<RunOutputs run_id={run.id} />
</Suspense>
</div>
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
</DialogContent>
</Dialog>
);
return (
<Dialog>
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<TableRow>
<TableCell>{run.number}</TableCell>
<TableCell className="font-medium truncate">
{run.machine?.name}
</TableCell>
<TableCell className="truncate">
{getRelativeTime(run.created_at)}
</TableCell>
<TableCell>{run.version?.version}</TableCell>
<TableCell>
<Badge variant="outline" className="truncate">
{run.origin}
</Badge>
</TableCell>
<TableCell className="truncate">
{getDuration(run.duration)}
</TableCell>
<LiveStatus run={run} />
</TableRow>
</DialogTrigger>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Run outputs</DialogTitle>
<DialogDescription>
You can view your run&apos;s outputs here
</DialogDescription>
</DialogHeader>
<div className="max-h-96 overflow-y-scroll">
<RunInputs run={run} />
<Suspense>
<RunOutputs run_id={run.id} />
</Suspense>
</div>
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
</DialogContent>
</Dialog>
);
}
+40 -40
View File
@@ -1,3 +1,10 @@
import {
findAllDeployments,
findAllRunsWithCounts,
} from "../server/findAllRuns";
import { DeploymentDisplay } from "./DeploymentDisplay";
import { PaginationControl } from "./PaginationControl";
import { RunDisplay } from "./RunDisplay";
import {
Table,
TableBody,
@@ -8,13 +15,6 @@ import {
} from "@/components/ui/table";
import { parseAsInteger } from "next-usequerystate";
import { headers } from "next/headers";
import {
findAllDeployments,
findAllRunsWithCounts,
} from "../server/findAllRuns";
import { DeploymentDisplay } from "./DeploymentDisplay";
import { PaginationControl } from "./PaginationControl";
import { RunDisplay } from "./RunDisplay";
const itemPerPage = 6;
const pageParser = parseAsInteger.withDefault(1);
@@ -33,40 +33,40 @@ export async function RunsTable(props: {
offset: (page - 1) * itemPerPage,
});
return (
<div>
<div className="overflow-auto h-fit w-full">
<Table className="">
{allRuns.length === 0 && (
<TableCaption>A list of your recent runs.</TableCaption>
)}
<TableHeader className="bg-background top-0 sticky">
<TableRow>
<TableHead className="truncate">Number</TableHead>
<TableHead className="truncate">Machine</TableHead>
<TableHead className="truncate">Time</TableHead>
<TableHead className="truncate">Version</TableHead>
<TableHead className="truncate">Origin</TableHead>
<TableHead className="truncate">Duration</TableHead>
<TableHead className="truncate">Live Status</TableHead>
<TableHead className="text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allRuns.map((run) => (
<RunDisplay run={run} key={run.id} />
))}
</TableBody>
</Table>
</div>
<div>
<div className="overflow-auto h-fit w-full">
<Table className="">
{allRuns.length == 0 && (
<TableCaption>A list of your recent runs.</TableCaption>
)}
<TableHeader className="bg-background top-0 sticky">
<TableRow>
<TableHead className="truncate">Number</TableHead>
<TableHead className="truncate">Machine</TableHead>
<TableHead className="truncate">Time</TableHead>
<TableHead className="truncate">Version</TableHead>
<TableHead className="truncate">Origin</TableHead>
<TableHead className="truncate">Duration</TableHead>
<TableHead className="truncate">Live Status</TableHead>
<TableHead className="text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allRuns.map((run) => (
<RunDisplay run={run} key={run.id} />
))}
</TableBody>
</Table>
</div>
{Math.ceil(total / itemPerPage) > 0 && (
<PaginationControl
totalPage={Math.ceil(total / itemPerPage)}
currentPage={page}
/>
)}
</div>
);
{Math.ceil(total / itemPerPage) > 0 && (
<PaginationControl
totalPage={Math.ceil(total / itemPerPage)}
currentPage={page}
/>
)}
</div>
);
}
export async function DeploymentsTable(props: { workflow_id: string }) {
+2 -3
View File
@@ -1,5 +1,6 @@
"use client";
import { useServerActionData } from "./useServerActionData";
import { ButtonAction } from "@/components/ButtonActionLoader";
import { UpdateModal } from "@/components/InsertModal";
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
@@ -14,7 +15,6 @@ import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useServerActionData } from "./useServerActionData";
export function SharePageSettings({
deployment_id,
@@ -58,14 +58,13 @@ export function SharePageSettings({
type="button"
>
<ButtonAction
routerAction="back"
action={removePublicShareDeployment.bind(null, deployment.id)}
>
Remove
</ButtonAction>
</Button>
<Button asChild className="gap-2 truncate" type="button">
<Link href={`/share/${deployment.share_slug ?? deployment.id}`} target="_blank">
<Link href={`/share/${deployment.id}`} target="_blank">
View Share Page <ExternalLink size={14} />
</Link>
</Button>
+57 -90
View File
@@ -1,5 +1,8 @@
"use client";
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
import { callServerPromise } from "./callServerPromise";
import fetcher from "./fetcher";
import { LoadingIcon } from "@/components/LoadingIcon";
import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
import { Badge } from "@/components/ui/badge";
@@ -43,11 +46,11 @@ import type { getMachines } from "@/server/curdMachine";
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
import {
Copy,
Edit,
ExternalLink,
Info,
MoreVertical,
Play,
Share,
} from "lucide-react";
import { parseAsInteger, useQueryState } from "next-usequerystate";
import { useEffect, useMemo, useState } from "react";
@@ -55,11 +58,6 @@ import { toast } from "sonner";
import useSWR from "swr";
import type { z } from "zod";
import { create } from "zustand";
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
import { callServerPromise } from "./callServerPromise";
import fetcher from "./fetcher";
import { ButtonAction } from "@/components/ButtonActionLoader";
import { editWorkflowOnMachine } from "@/server/editWorkflowOnMachine";
export function VersionSelect({
workflow,
@@ -124,48 +122,12 @@ export function MachineSelect({
);
}
type SelectedMachineStore = {
selectedMachine: string | undefined;
setSelectedMachine: (machine: string) => void;
};
function useSelectedMachine(machines: Awaited<ReturnType<typeof getMachines>>) {
const a = useQueryState("machine", {
defaultValue: machines?.[0]?.id ?? "",
});
export const selectedMachineStore = create<SelectedMachineStore>((set) => ({
selectedMachine: undefined,
setSelectedMachine: (machine) => set(() => ({ selectedMachine: machine })),
}));
export function useSelectedMachine(
machines: Awaited<ReturnType<typeof getMachines>>,
): [string, (v: string) => void] {
const { selectedMachine, setSelectedMachine } = selectedMachineStore();
return [selectedMachine ?? machines?.[0]?.id ?? "", setSelectedMachine];
// 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));
// },
// ];
return a;
}
type PublicRunStore = {
@@ -262,7 +224,7 @@ export function RunWorkflowButton({
const schema = useMemo(() => {
const workflow_version = getWorkflowVersionFromVersionIndex(
workflow,
version,
version
);
if (!workflow_version) return null;
@@ -276,7 +238,7 @@ export function RunWorkflowButton({
const val = Object.keys(values).length > 0 ? values : undefined;
const workflow_version_id = workflow?.versions.find(
(x) => x.version === version,
(x) => x.version === version
)?.id;
console.log(workflow_version_id);
if (!workflow_version_id) return;
@@ -291,7 +253,7 @@ export function RunWorkflowButton({
machine_id: machine,
inputs: val,
runOrigin: "manual",
}),
})
);
// console.log(res.json());
setIsLoading(false);
@@ -360,7 +322,7 @@ export function CreateDeploymentButton({
const [isLoading, setIsLoading] = useState(false);
const workflow_version_id = workflow?.versions.find(
(x) => x.version === version,
(x) => x.version === version
)?.id;
return (
<DropdownMenu>
@@ -380,8 +342,8 @@ export function CreateDeploymentButton({
workflow.id,
workflow_version_id,
machine,
"production",
),
"production"
)
);
setIsLoading(false);
}}
@@ -398,8 +360,8 @@ export function CreateDeploymentButton({
workflow.id,
workflow_version_id,
machine,
"staging",
),
"staging"
)
);
setIsLoading(false);
}}
@@ -411,7 +373,7 @@ export function CreateDeploymentButton({
);
}
export function OpenEditButton({
export function CreateShareButton({
workflow,
machines,
}: {
@@ -423,34 +385,40 @@ export function OpenEditButton({
...parseAsInteger,
});
const [machine] = useSelectedMachine(machines);
const workflow_version_id = workflow?.versions.find(
(x) => x.version == version,
)?.id;
const [isLoading, setIsLoading] = useState(false);
const workflow_version_id = workflow?.versions.find(
(x) => x.version === version
)?.id;
return (
workflow_version_id &&
machine && (
<Button
className="gap-2"
onClick={async () => {
setIsLoading(true);
const url = await callServerPromise(
editWorkflowOnMachine(workflow_version_id, machine),
);
if (url && typeof url !== "object") {
window.open(url, "_blank");
} else if (url && typeof url === "object" && url.error) {
console.error(url.error);
}
setIsLoading(false);
}}
// asChild
variant="outline"
>
Edit {isLoading ? <LoadingIcon /> : <Edit size={14} />}
</Button>
)
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="gap-2" disabled={isLoading} variant="outline">
Share {isLoading ? <LoadingIcon /> : <Share size={14} />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem
onClick={async () => {
if (!workflow_version_id) return;
setIsLoading(true);
await callServerPromise(
createDeployments(
workflow.id,
workflow_version_id,
machine,
"public-share"
)
);
setIsLoading(false);
}}
>
Public
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -464,7 +432,7 @@ export function CopyWorkflowVersion({
...parseAsInteger,
});
const workflow_version = workflow?.versions.find(
(x) => x.version === version,
(x) => x.version === version
);
return (
<DropdownMenu>
@@ -488,7 +456,7 @@ export function CopyWorkflowVersion({
});
navigator.clipboard.writeText(
JSON.stringify(workflow_version?.workflow),
JSON.stringify(workflow_version?.workflow)
);
toast("Copied to clipboard");
}}
@@ -498,7 +466,7 @@ export function CopyWorkflowVersion({
<DropdownMenuItem
onClick={async () => {
navigator.clipboard.writeText(
JSON.stringify(workflow_version?.workflow_api),
JSON.stringify(workflow_version?.workflow_api)
);
toast("Copied to clipboard");
}}
@@ -512,7 +480,7 @@ export function CopyWorkflowVersion({
export function getWorkflowVersionFromVersionIndex(
workflow: Awaited<ReturnType<typeof findFirstTableWithVersion>>,
version: number,
version: number
) {
const workflow_version = workflow?.versions.find((x) => x.version == version);
@@ -538,7 +506,7 @@ export function ViewWorkflowDetailsButton({
isLoading: isNodesIndexLoading,
} = useSWR(
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/extension-node-map.json",
fetcher,
fetcher
);
const groupedByAuxName = useMemo(() => {
@@ -548,7 +516,7 @@ export function ViewWorkflowDetailsButton({
const workflow_version = getWorkflowVersionFromVersionIndex(
workflow,
version,
version
);
const api = workflow_version?.workflow_api;
@@ -559,7 +527,7 @@ export function ViewWorkflowDetailsButton({
.map(([_, value]) => {
const classType = value.class_type;
const classTypeData = Object.entries(data).find(([_, nodeArray]) =>
nodeArray[0].includes(classType),
nodeArray[0].includes(classType)
);
return classTypeData ? { node: value, classTypeData } : null;
})
@@ -589,7 +557,7 @@ export function ViewWorkflowDetailsButton({
node: z.infer<typeof workflowAPINodeType>[];
url: string;
}
>,
>
);
// console.log(groupedByAuxName);
@@ -631,7 +599,6 @@ export function ViewWorkflowDetailsButton({
href={group.url}
target="_blank"
className="hover:underline"
rel="noreferrer"
>
{key}
<ExternalLink
+4 -4
View File
@@ -21,8 +21,8 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getAllUserWorkflow } from "@/server/crudWorkflow";
import { deleteWorkflow } from "@/server/deleteWorkflow";
import type { getAllUserWorkflow } from "@/server/getAllUserWorkflow";
import type {
ColumnDef,
ColumnFiltersState,
@@ -169,7 +169,7 @@ export const columns: ColumnDef<WorkflowItemList>[] = [
export function WorkflowList({ data }: { data: WorkflowItemList[] }) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
@@ -218,7 +218,7 @@ export function WorkflowList({ data }: { data: WorkflowItemList[] }) {
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
header.getContext()
)}
</TableHead>
);
@@ -237,7 +237,7 @@ export function WorkflowList({ data }: { data: WorkflowItemList[] }) {
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
cell.getContext()
)}
</TableCell>
))}
+104 -102
View File
@@ -42,105 +42,105 @@ const Model = z.object({
url: z.string(),
});
export const CivitalModelSchema = z.object({
items: z.array(
export const CivitaiModel = z.object({
id: z.number(),
name: z.string(),
description: z.string(),
type: z.string(),
// poi: z.boolean(),
// nsfw: z.boolean(),
// allowNoCredit: z.boolean(),
// allowCommercialUse: z.string(),
// allowDerivatives: z.boolean(),
// allowDifferentLicense: z.boolean(),
// stats: z.object({
// downloadCount: z.number(),
// favoriteCount: z.number(),
// commentCount: z.number(),
// ratingCount: z.number(),
// rating: z.number(),
// tippedAmountCount: z.number(),
// }),
creator: z
.object({
username: z.string().nullable(),
image: z.string().nullable().default(null),
})
.nullable(),
tags: z.array(z.string()),
modelVersions: z.array(
z.object({
id: z.number(),
modelId: z.number(),
name: z.string(),
description: z.string(),
type: z.string(),
// poi: z.boolean(),
// nsfw: z.boolean(),
// allowNoCredit: z.boolean(),
// allowCommercialUse: z.string(),
// allowDerivatives: z.boolean(),
// allowDifferentLicense: z.boolean(),
// stats: z.object({
// downloadCount: z.number(),
// favoriteCount: z.number(),
// commentCount: z.number(),
// ratingCount: z.number(),
// rating: z.number(),
// tippedAmountCount: z.number(),
// }),
creator: z
.object({
username: z.string().nullable(),
image: z.string().nullable().default(null),
})
.nullable(),
tags: z.array(z.string()),
modelVersions: z.array(
createdAt: z.string(),
updatedAt: z.string(),
status: z.string(),
publishedAt: z.string(),
trainedWords: z.array(z.unknown()),
trainingStatus: z.string().nullable(),
trainingDetails: z.string().nullable(),
baseModel: z.string(),
baseModelType: z.string().nullable(),
earlyAccessTimeFrame: z.number(),
description: z.string().nullable(),
vaeId: z.number().nullable(),
stats: z.object({
downloadCount: z.number(),
ratingCount: z.number(),
rating: z.number(),
}),
files: z.array(
z.object({
id: z.number(),
modelId: z.number(),
sizeKB: z.number(),
name: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
status: z.string(),
publishedAt: z.string(),
trainedWords: z.array(z.unknown()),
trainingStatus: z.string().nullable(),
trainingDetails: z.string().nullable(),
baseModel: z.string(),
baseModelType: z.string().nullable(),
earlyAccessTimeFrame: z.number(),
description: z.string().nullable(),
vaeId: z.number().nullable(),
stats: z.object({
downloadCount: z.number(),
ratingCount: z.number(),
rating: z.number(),
}),
files: z.array(
z.object({
id: z.number(),
sizeKB: z.number(),
name: z.string(),
type: z.string(),
// metadata: z.object({
// fp: z.string().nullable().optional(),
// size: z.string().nullable().optional(),
// format: z.string().nullable().optional(),
// }),
// pickleScanResult: z.string(),
// pickleScanMessage: z.string(),
// virusScanResult: z.string(),
// virusScanMessage: z.string().nullable(),
// scannedAt: z.string(),
// hashes: z.object({
// AutoV1: z.string().nullable().optional(),
// AutoV2: z.string().nullable().optional(),
// SHA256: z.string().nullable().optional(),
// CRC32: z.string().nullable().optional(),
// BLAKE3: z.string().nullable().optional(),
// }),
downloadUrl: z.string(),
// primary: z.boolean().default(false),
})
),
images: z.array(
z.object({
id: z.number(),
url: z.string(),
nsfw: z.string(),
width: z.number(),
height: z.number(),
hash: z.string(),
type: z.string(),
metadata: z.object({
hash: z.string(),
width: z.number(),
height: z.number(),
}),
meta: z.any(),
})
),
type: z.string(),
// metadata: z.object({
// fp: z.string().nullable().optional(),
// size: z.string().nullable().optional(),
// format: z.string().nullable().optional(),
// }),
// pickleScanResult: z.string(),
// pickleScanMessage: z.string(),
// virusScanResult: z.string(),
// virusScanMessage: z.string().nullable(),
// scannedAt: z.string(),
// hashes: z.object({
// AutoV1: z.string().nullable().optional(),
// AutoV2: z.string().nullable().optional(),
// SHA256: z.string().nullable().optional(),
// CRC32: z.string().nullable().optional(),
// BLAKE3: z.string().nullable().optional(),
// }),
downloadUrl: z.string(),
})
// primary: z.boolean().default(false),
}),
),
})
images: z.array(
z.object({
id: z.number(),
url: z.string(),
nsfw: z.string(),
width: z.number(),
height: z.number(),
hash: z.string(),
type: z.string(),
metadata: z.object({
hash: z.string(),
width: z.number(),
height: z.number(),
}),
meta: z.any(),
}),
),
downloadUrl: z.string(),
}),
),
});
export const CivitalModelSchema = z.object({
items: z.array(CivitaiModel),
metadata: z.object({
totalItems: z.number(),
currentPage: z.number(),
@@ -197,7 +197,7 @@ function mapType(type: string) {
}
function mapModelsList(
models: z.infer<typeof CivitalModelSchema>
models: z.infer<typeof CivitalModelSchema>,
): z.infer<typeof ModelListWrapper> {
return {
models: models.items.flatMap((item) => {
@@ -241,8 +241,9 @@ function getUrl(search?: string) {
export function CivitaiModelRegistry({
field,
}: Pick<AutoFormInputComponentProps, "field">) {
const [modelList, setModelList] =
React.useState<z.infer<typeof ModelListWrapper>>();
const [modelList, setModelList] = React.useState<
z.infer<typeof ModelListWrapper>
>();
const [loading, setLoading] = React.useState(false);
@@ -301,8 +302,9 @@ export function CivitaiModelRegistry({
export function ComfyUIManagerModelRegistry({
field,
}: Pick<AutoFormInputComponentProps, "field">) {
const [modelList, setModelList] =
React.useState<z.infer<typeof ModelListWrapper>>();
const [modelList, setModelList] = React.useState<
z.infer<typeof ModelListWrapper>
>();
React.useEffect(() => {
const controller = new AbortController();
@@ -310,7 +312,7 @@ export function ComfyUIManagerModelRegistry({
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/model-list.json",
{
signal: controller.signal,
}
},
)
.then((x) => x.json())
.then((a) => {
@@ -353,14 +355,14 @@ export function ModelSelector({
if (
prevSelectedModels.some(
(selectedModel) =>
selectedModel.url + selectedModel.name === model.url + model.name
selectedModel.url + selectedModel.name === model.url + model.name,
)
) {
field.onChange(
prevSelectedModels.filter(
(selectedModel) =>
selectedModel.url + selectedModel.name !== model.url + model.name
)
selectedModel.url + selectedModel.name !== model.url + model.name,
),
);
} else {
field.onChange([...prevSelectedModels, model]);
@@ -408,10 +410,10 @@ export function ModelSelector({
className={cn(
"ml-auto h-4 w-4",
value.some(
(selectedModel) => selectedModel.url === model.url
)
(selectedModel) => selectedModel.url === model.url,
)
? "opacity-100"
: "opacity-0"
: "opacity-0",
)}
/>
</CommandItem>
@@ -0,0 +1,89 @@
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import { FormControl, FormItem, FormLabel } from "../ui/form";
import { LoadingIcon } from "@/components/LoadingIcon";
import * as React from "react";
import AutoFormInput from "../ui/auto-form/fields/input";
import { useDebouncedCallback } from "use-debounce";
import { CivitaiModel } from "./ModelPickerView";
import { z } from "zod";
import { insertCivitaiCheckpointSchema } from "@/db/schema";
function getUrl(civitai_url: string) {
// expect to be a URL to be https://civitai.com/models/36520
// possiblity with slugged name and query-param modelVersionId
const baseUrl = "https://civitai.com/api/v1/models/";
const url = new URL(civitai_url);
const pathSegments = url.pathname.split("/");
const modelId = pathSegments[pathSegments.indexOf("models") + 1];
const modelVersionId = url.searchParams.get("modelVersionId");
return { url: baseUrl + modelId, modelVersionId };
}
export default function AutoFormCheckpointInput(
props: AutoFormInputComponentProps,
) {
const [loading, setLoading] = React.useState(false);
const [modelRes, setModelRes] = React.useState<
z.infer<typeof CivitaiModel>
>();
const [modelVersionid, setModelVersionId] = React.useState<string | null>();
const { label, isRequired, fieldProps, zodItem, fieldConfigItem } = props;
const handleSearch = useDebouncedCallback((search) => {
const validationResult = insertCivitaiCheckpointSchema.shape.civitai_url
.safeParse(search);
if (!validationResult.success) {
console.error(validationResult.error);
// Optionally set an error state here
return;
}
setLoading(true);
const controller = new AbortController();
const { url, modelVersionId: versionId } = getUrl(search);
setModelVersionId(versionId);
fetch(url, {
signal: controller.signal,
})
.then((x) => x.json())
.then((a) => {
const res = CivitaiModel.parse(a);
console.log(a);
console.log(res);
setModelRes(res);
setLoading(false);
});
return () => {
controller.abort();
setLoading(false);
};
}, 300);
const modifiedField = {
...fieldProps,
// onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
// handleSearch(event.target.value);
// },
};
return (
<FormItem>
{fieldConfigItem.inputProps?.showLabel && (
<FormLabel>
{label}
{isRequired && <span className="text-destructive">*</span>}
</FormLabel>
)}
<FormControl>
<AutoFormInput
{...props}
fieldProps={modifiedField}
/>
</FormControl>
</FormItem>
);
}
-1
View File
@@ -5,5 +5,4 @@ export const customInputNodes: Record<string, string> = {
ComfyUIDeployExternalNumber: "float",
ComfyUIDeployExternalNumberInt: "integer",
ComfyUIDeployExternalLora: "string - (public lora download url)",
ComfyUIDeployExternalCheckpoint: "string - (public checkpoints download url)",
};
+1 -1
View File
@@ -69,7 +69,7 @@ const FeedbackThanks = forwardRef<React.ElementRef<'div'>>(
)
export function Feedback() {
const [submitted, setSubmitted] = useState(false)
let [submitted, setSubmitted] = useState(false)
function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
+1 -1
View File
@@ -8,7 +8,7 @@ export function Prose<T extends React.ElementType = 'div'>({
as?: T
className?: string
}) {
const Component = as ?? 'div'
let Component = as ?? 'div'
return (
<Component
+3 -3
View File
@@ -22,9 +22,9 @@ function MoonIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
}
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme()
const otherTheme = resolvedTheme === 'dark' ? 'light' : 'dark'
const [mounted, setMounted] = useState(false)
let { resolvedTheme, setTheme } = useTheme()
let otherTheme = resolvedTheme === 'dark' ? 'light' : 'dark'
let [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
@@ -8,6 +8,7 @@ import AutoFormSwitch from "./fields/switch";
import AutoFormTextarea from "./fields/textarea";
import AutoFormModelsPicker from "@/components/custom-form/model-picker";
import AutoFormSnapshotPicker from "@/components/custom-form/snapshot-picker";
import AutoFormCheckpointInput from "@/components/custom-form/checkpoint-input";
export const INPUT_COMPONENTS = {
checkbox: AutoFormCheckbox,
@@ -22,6 +23,7 @@ export const INPUT_COMPONENTS = {
// Customs
snapshot: AutoFormSnapshotPicker,
models: AutoFormModelsPicker,
checkpoints: AutoFormCheckpointInput,
};
/**
+4 -12
View File
@@ -1,19 +1,11 @@
import { Pool, neonConfig } from "@neondatabase/serverless";
import { drizzle as neonDrizzle } from "drizzle-orm/neon-serverless";
import * as schema from "./schema";
const isDevContainer = process.env.REMOTE_CONTAINERS !== undefined;
import { neonConfig, Pool } from "@neondatabase/serverless";
import { drizzle as neonDrizzle } from "drizzle-orm/neon-serverless";
// if we're running locally
if (process.env.VERCEL_ENV !== "production") {
// Set the WebSocket proxy to work with the local instance
if (isDevContainer) {
// Running inside a VS Code devcontainer
neonConfig.wsProxy = (host) => "host.docker.internal:5481/v1";
} else {
// Not running inside a VS Code devcontainer
neonConfig.wsProxy = (host) => `${host}:5481/v1`;
}
neonConfig.wsProxy = (host) => `${host}:5481/v1`;
// Disable all authentication and encryption
neonConfig.useSecureWebSocket = false;
neonConfig.pipelineTLS = false;
@@ -26,5 +18,5 @@ export const db = neonDrizzle(
}),
{
schema,
},
}
);
+102 -15
View File
@@ -1,3 +1,4 @@
import { CivitaiModelResponse } from "@/types/civitai";
import { type InferSelectModel, relations } from "drizzle-orm";
import {
boolean,
@@ -9,7 +10,7 @@ import {
timestamp,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
export const dbSchema = pgSchema("comfyui_deploy");
@@ -35,8 +36,6 @@ export const workflowTable = dbSchema.table("workflows", {
updated_at: timestamp("updated_at").defaultNow().notNull(),
});
export const workflowSchema = createSelectSchema(workflowTable);
export const workflowRelations = relations(workflowTable, ({ many, one }) => ({
user: one(usersTable, {
fields: [workflowTable.user_id],
@@ -81,7 +80,6 @@ export const workflowVersionTable = dbSchema.table("workflow_versions", {
created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(),
});
export const workflowVersionSchema = createSelectSchema(workflowVersionTable);
export const workflowVersionRelations = relations(
workflowVersionTable,
@@ -141,8 +139,9 @@ export const workflowRunsTable = dbSchema.table("workflow_runs", {
onDelete: "set null",
},
),
workflow_inputs:
jsonb("workflow_inputs").$type<Record<string, string | number>>(),
workflow_inputs: jsonb("workflow_inputs").$type<
Record<string, string | number>
>(),
workflow_id: uuid("workflow_id")
.notNull()
.references(() => workflowTable.id, {
@@ -156,7 +155,6 @@ export const workflowRunsTable = dbSchema.table("workflow_runs", {
status: workflowRunStatus("status").notNull().default("not-started"),
ended_at: timestamp("ended_at"),
created_at: timestamp("created_at").defaultNow().notNull(),
started_at: timestamp("started_at"),
});
export const workflowRunRelations = relations(
@@ -278,10 +276,10 @@ export const deploymentsTable = dbSchema.table("deployments", {
machine_id: uuid("machine_id")
.notNull()
.references(() => machinesTable.id),
share_slug: text("share_slug").unique(),
description: text("description"),
showcase_media:
jsonb("showcase_media").$type<z.infer<typeof showcaseMedia>>(),
showcase_media: jsonb("showcase_media").$type<
z.infer<typeof showcaseMedia>
>(),
environment: deploymentEnvironment("environment").notNull(),
created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(),
@@ -334,18 +332,107 @@ export const apiKeyTable = dbSchema.table("api_keys", {
updated_at: timestamp("updated_at").defaultNow().notNull(),
});
export const authRequestsTable = dbSchema.table("auth_requests", {
request_id: text("request_id").primaryKey().notNull(),
user_id: text("user_id"),
export const resourceUpload = pgEnum("resource_upload", [
"started",
"error",
"succeded",
]);
export const modelUploadType = pgEnum("model_upload_type", [
"civitai",
"huggingface",
"other",
]);
export const checkpointTable = dbSchema.table("checkpoints", {
id: uuid("id").primaryKey().defaultRandom().notNull(),
user_id: text("user_id")
.references(() => usersTable.id, {}), // perhaps a "special" user_id for global checkpoints
org_id: text("org_id"),
api_hash: text("api_hash"),
description: text("description"),
checkpoint_volume_id: uuid("checkpoint_volume_id")
.notNull()
.references(() => workflowRunsTable.id, {
onDelete: "cascade",
}).notNull(),
model_name: text("model_name"),
civitai_id: text("civitai_id"),
civitai_version_id: text("civitai_version_id"),
civitai_url: text("civitai_url"),
civitai_download_url: text("civitai_download_url"),
civitai_model_response: jsonb("civitai_model_response").$type<
z.infer<typeof CivitaiModelResponse>
>(),
hf_url: text("hf_url"),
s3_url: text("s3_url"),
user_url: text("client_url"),
is_public: boolean("is_public").notNull().default(false),
status: resourceUpload("status").notNull().default("started"),
upload_machine_id: text("upload_machine_id"),
upload_type: modelUploadType("upload_type").notNull(),
build_log: text("build_log"),
created_at: timestamp("created_at").defaultNow().notNull(),
expired_date: timestamp("expired_date"),
updated_at: timestamp("updated_at").defaultNow().notNull(),
});
export const insertCivitaiCheckpointSchema = createInsertSchema(
checkpointTable,
{
civitai_url: (schema) =>
schema.civitai_url.trim().url({ message: "URL required" }).includes(
"civitai.com/models",
{ message: "civitai.com/models link required" },
),
},
);
export const checkpointVolumeTable = dbSchema.table("checkpoint_volume", {
id: uuid("id").primaryKey().defaultRandom().notNull(),
user_id: text("user_id")
.references(() => usersTable.id, {
// onDelete: "cascade",
}),
org_id: text("org_id"),
volume_name: text("volume_name").notNull(),
created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(),
disabled: boolean("disabled").default(false).notNull(),
});
export const checkpointRelations = relations(checkpointTable, ({ one }) => ({
user: one(usersTable, {
fields: [checkpointTable.user_id],
references: [usersTable.id],
}),
volume: one(checkpointVolumeTable, {
fields: [checkpointTable.checkpoint_volume_id],
references: [checkpointVolumeTable.id],
}),
}));
export const checkpointVolumeRelations = relations(
checkpointVolumeTable,
({ many, one }) => ({
checkpoint: many(checkpointTable),
user: one(usersTable, {
fields: [checkpointVolumeTable.user_id],
references: [usersTable.id],
}),
}),
);
export type UserType = InferSelectModel<typeof usersTable>;
export type WorkflowType = InferSelectModel<typeof workflowTable>;
export type MachineType = InferSelectModel<typeof machinesTable>;
export type WorkflowVersionType = InferSelectModel<typeof workflowVersionTable>;
export type DeploymentType = InferSelectModel<typeof deploymentsTable>;
export type CheckpointType = InferSelectModel<typeof checkpointTable>;
export type CheckpointVolumeType = InferSelectModel<
typeof checkpointVolumeTable
>;
+4 -6
View File
@@ -1,10 +1,8 @@
export function remToPx(remValue: number) {
const rootFontSize =
typeof window === "undefined"
? 16
: parseFloat(
window.getComputedStyle(document.documentElement).fontSize,
);
let rootFontSize =
typeof window === 'undefined'
? 16
: parseFloat(window.getComputedStyle(document.documentElement).fontSize)
return remValue * rootFontSize
}
+10 -10
View File
@@ -27,13 +27,13 @@ function rehypeShiki() {
visit(tree, "element", (node) => {
if (node.tagName === "pre" && node.children[0]?.tagName === "code") {
const codeNode = node.children[0];
const textNode = codeNode.children[0];
let codeNode = node.children[0];
let textNode = codeNode.children[0];
node.properties.code = textNode.value;
if (node.properties.language) {
const tokens = highlighter.codeToThemedTokens(
let tokens = highlighter.codeToThemedTokens(
textNode.value,
node.properties.language
);
@@ -53,7 +53,7 @@ function rehypeShiki() {
function rehypeSlugify() {
return (tree) => {
const slugify = slugifyWithCounter();
let slugify = slugifyWithCounter();
visit(tree, "element", (node) => {
if (node.tagName === "h2" && !node.properties.id) {
node.properties.id = slugify(toString(node));
@@ -64,10 +64,10 @@ function rehypeSlugify() {
function rehypeAddMDXExports(getExports) {
return (tree) => {
const exports = Object.entries(getExports(tree));
let exports = Object.entries(getExports(tree));
for (const [name, value] of exports) {
for (const node of tree.children) {
for (let [name, value] of exports) {
for (let node of tree.children) {
if (
node.type === "mdxjsEsm" &&
new RegExp(`export\\s+const\\s+${name}\\s*=`).test(node.value)
@@ -76,7 +76,7 @@ function rehypeAddMDXExports(getExports) {
}
}
const exportStr = `export const ${name} = ${value}`;
let exportStr = `export const ${name} = ${value}`;
tree.children.push({
type: "mdxjsEsm",
@@ -93,9 +93,9 @@ function rehypeAddMDXExports(getExports) {
}
function getSections(node) {
const sections = [];
let sections = [];
for (const child of node.children ?? []) {
for (let child of node.children ?? []) {
if (child.type === "element" && child.tagName === "h2") {
sections.push(`{
title: ${JSON.stringify(toString(child))},
+8 -8
View File
@@ -31,9 +31,9 @@ function extractSections() {
visit(tree, (node) => {
if (node.type === "heading" || node.type === "paragraph") {
const content = toString(excludeObjectExpressions(node));
let content = toString(excludeObjectExpressions(node));
if (node.type === "heading" && node.depth <= 2) {
const hash = node.depth === 1 ? null : slugify(content);
let hash = node.depth === 1 ? null : slugify(content);
sections.push([content, hash, []]);
} else {
sections.at(-1)?.[2].push(content);
@@ -45,7 +45,7 @@ function extractSections() {
}
export default function (nextConfig = {}) {
const cache = new Map();
let cache = new Map();
return Object.assign({}, nextConfig, {
webpack(config, options) {
@@ -53,20 +53,20 @@ export default function (nextConfig = {}) {
test: __filename,
use: [
createLoader(function () {
const appDir = path.resolve("./src/app/(docs)/docs");
let appDir = path.resolve("./src/app/(docs)/docs");
this.addContextDependency(appDir);
const files = glob.sync("**/*.mdx", { cwd: appDir });
const data = files.map((file) => {
let files = glob.sync("**/*.mdx", { cwd: appDir });
let data = files.map((file) => {
let url = `/${file.replace(/(^|\/)page\.mdx$/, "")}`;
const mdx = fs.readFileSync(path.join(appDir, file), "utf8");
let mdx = fs.readFileSync(path.join(appDir, file), "utf8");
let sections = [];
if (cache.get(file)?.[0] === mdx) {
sections = cache.get(file)[1];
} else {
const vfile = { value: mdx, sections };
let vfile = { value: mdx, sections };
processor.runSync(processor.parse(vfile), vfile);
cache.set(file, [mdx, sections]);
}
-2
View File
@@ -1,6 +1,4 @@
import type { ResponseConfig } from "@asteasolutions/zod-to-openapi";
import { z } from "@hono/zod-openapi";
export const authError = {
-13
View File
@@ -1,13 +0,0 @@
import { customAlphabet } from "nanoid";
export const nanoid = customAlphabet(
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
);
const prefixes = {
img: "img",
vid: "vid",
} as const;
export function newId(prefix: keyof typeof prefixes): string {
return [prefixes[prefix], nanoid(16)].join("_");
}
+3 -3
View File
@@ -1,10 +1,10 @@
import { createRun } from "../server/createRun";
import { db } from "@/db/db";
import { deploymentsTable } from "@/db/schema";
import type { App } from "@/routes/app";
import { authError } from "@/routes/authError";
import { createRoute, z } from "@hono/zod-openapi";
import { z, createRoute } from "@hono/zod-openapi";
import { eq } from "drizzle-orm";
import { createRun } from "../server/createRun";
const createRunRoute = createRoute({
method: "post",
@@ -99,7 +99,7 @@ export const registerCreateRunRoute = (app: App) => {
},
{
status: 500,
},
}
);
}
});
-150
View File
@@ -1,150 +0,0 @@
import { db } from "@/db/db";
import { authRequestsTable } from "@/db/schema";
import type { App } from "@/routes/app";
import { authError } from "@/routes/authError";
import { z, createRoute } from "@hono/zod-openapi";
import { eq } from "drizzle-orm";
import jwt from "jsonwebtoken";
import crypto from "crypto";
import { getOrgOrUserDisplayName } from "@/server/getOrgOrUserDisplayName";
import ms from "ms";
const route = createRoute({
method: "get",
path: "/auth-response/:request_id",
tags: ["comfyui"],
summary: "Get an API Key with code",
description:
"This endpoints is specifically built for ComfyUI workflow upload.",
request: {
params: z.object({
request_id: z.string(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: z.object({
api_key: z.string(),
name: z.string(),
}),
},
},
description: "The returned API Key",
},
201: {
content: {
"application/json": {
schema: z.object({
message: z.string(),
}),
},
},
description: "The API key is not yet ready",
},
500: {
content: {
"application/json": {
schema: z.object({
error: z.string(),
}),
},
},
description: "Error when fetching the API Key with code",
},
...authError,
},
});
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
export const registerGetAuthResponse = (app: App) => {
return app.openapi(route, async (c) => {
const { request_id } = c.req.valid("param");
try {
const result = await db.query.authRequestsTable.findFirst({
where: eq(authRequestsTable.request_id, request_id),
});
if (result?.api_hash) {
return c.json(
{
message: "Already used.",
},
{
status: 201,
headers: corsHeaders,
},
);
}
if (result && result.user_id) {
const expireTime = "1w";
const token = jwt.sign(
{ user_id: result.user_id, org_id: result.org_id },
process.env.JWT_SECRET!,
{
expiresIn: expireTime,
},
);
const hash = crypto.createHash("sha256").update(token).digest("hex");
const now = new Date();
const expiryDate = new Date(now.getTime() + ms(expireTime));
await db
.update(authRequestsTable)
.set({
api_hash: hash,
expired_date: expiryDate,
})
.where(eq(authRequestsTable.request_id, request_id));
const userName = await getOrgOrUserDisplayName(
result.org_id,
result.user_id,
);
return c.json(
{
api_key: token,
name: userName,
},
{
status: 200,
headers: corsHeaders,
},
);
}
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return c.json(
{
error: errorMessage,
},
{
statusText: "Invalid request",
status: 500,
headers: corsHeaders,
},
);
}
return c.json(
{
message: "Not ready yet.",
},
{
status: 201,
headers: corsHeaders,
},
);
});
};
-100
View File
@@ -1,100 +0,0 @@
import { workflowVersionSchema, workflowVersionTable } from "@/db/schema";
import type { App } from "@/routes/app";
import { authError } from "@/routes/authError";
import { getWorkflowVersion } from "@/server/crudWorkflow";
import { z, createRoute } from "@hono/zod-openapi";
import { createSelectSchema } from "drizzle-zod";
const route = createRoute({
method: "get",
path: "/workflow-version/:version_id",
tags: ["comfyui"],
summary: "Get comfyui workflow",
description: "Use this to retrieve comfyui workflow by id",
request: {
params: z.object({
version_id: z.string(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: createSelectSchema(workflowVersionTable, {
workflow_api: (schema) =>
schema.workflow_api.openapi({
type: "object",
}),
workflow: (schema) =>
schema.workflow.openapi({
type: "object",
}),
snapshot: (schema) =>
schema.snapshot.openapi({
type: "object",
}),
}),
},
},
description: "Retrieve the output",
},
500: {
content: {
"application/json": {
schema: z.object({
error: z.string(),
}),
},
},
description: "Error when uploading the workflow",
},
...authError,
},
});
export const registerGetWorkflowRoute = (app: App) => {
return app.openapi(route, async (c) => {
const { version_id } = c.req.valid("param");
const apiUser = c.get("apiKeyTokenData")!;
if (!apiUser.user_id)
return c.json(
{
error: "Invalid user_id",
},
{
status: 500,
},
);
try {
const workflow_version = await getWorkflowVersion(apiUser, version_id);
if (workflow_version) {
return c.json(workflow_version, {
status: 200,
});
} else {
return c.json(
{
error: "No version found",
},
{
status: 500,
},
);
}
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return c.json(
{
error: errorMessage,
},
{
statusText: "Invalid request",
status: 500,
},
);
}
});
};
+16 -3
View File
@@ -3,7 +3,20 @@ import { authError } from "@/routes/authError";
import { getFileDownloadUrl } from "@/server/getFileDownloadUrl";
import { handleResourceUpload } from "@/server/resource";
import { z, createRoute } from "@hono/zod-openapi";
import { newId } from "./newId";
import { customAlphabet } from "nanoid";
export const nanoid = customAlphabet(
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
);
const prefixes = {
img: "img",
vid: "vid",
} as const;
export function newId(prefix: keyof typeof prefixes): string {
return [prefixes[prefix], nanoid(16)].join("_");
}
const uploadUrlRoute = createRoute({
method: "get",
@@ -83,7 +96,7 @@ export const registerUploadRoute = (app: App) => {
file_id: id,
download_url: await getFileDownloadUrl(filePath),
},
200,
200
);
} catch (error: unknown) {
const errorMessage =
@@ -94,7 +107,7 @@ export const registerUploadRoute = (app: App) => {
},
{
status: 500,
},
}
);
}
});
@@ -1,197 +0,0 @@
import { db } from "@/db/db";
import {
snapshotType,
workflowAPIType,
workflowTable,
workflowType,
} from "@/db/schema";
import type { App } from "@/routes/app";
import { authError } from "@/routes/authError";
import {
createNewWorkflow,
createNewWorkflowVersion,
} from "@/server/createNewWorkflow";
import { z, createRoute } from "@hono/zod-openapi";
import { and, eq, isNull } from "drizzle-orm";
const route = createRoute({
method: "post",
path: "/workflow",
tags: ["comfyui"],
summary: "Upload workflow from ComfyUI",
description:
"This endpoints is specifically built for ComfyUI workflow upload.",
request: {
body: {
content: {
"application/json": {
schema: z.object({
workflow_id: z.string().optional(),
workflow_name: z.string().min(1).optional(),
workflow: workflowType,
workflow_api: workflowAPIType,
snapshot: snapshotType,
}),
},
},
},
},
responses: {
200: {
content: {
"application/json": {
schema: z.object({
workflow_id: z.string(),
version: z.string(),
}),
},
},
description: "Retrieve the output",
},
500: {
content: {
"application/json": {
schema: z.object({
error: z.string(),
}),
},
},
description: "Error when uploading the workflow",
},
...authError,
},
});
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
export const registerWorkflowUploadRoute = (app: App) => {
app.openapi(route, async (c) => {
const {
// user_id,
workflow,
workflow_api,
workflow_id: _workflow_id,
workflow_name,
snapshot,
} = c.req.valid("json");
const { org_id, user_id } = c.get("apiKeyTokenData")!;
if (!user_id)
return c.json(
{
error: "Invalid user_id",
},
{
headers: corsHeaders,
status: 500,
},
);
let workflow_id = _workflow_id;
let version = -1;
try {
if ((!workflow_id || workflow_id.length === 0) && workflow_name) {
// Create a new parent workflow
const { workflow_id: _workflow_id, version: _version } =
await createNewWorkflow({
user_id: user_id,
org_id: org_id,
workflow_name: workflow_name,
workflowData: {
workflow,
workflow_api,
snapshot,
},
});
workflow_id = _workflow_id;
version = _version;
} else if (workflow_id) {
const _workflow = await db
.select()
.from(workflowTable)
.where(
and(
eq(workflowTable.id, workflow_id),
eq(workflowTable.user_id, user_id),
org_id
? eq(workflowTable.org_id, org_id)
: isNull(workflowTable.org_id),
),
);
if (_workflow.length === 0) {
return c.json(
{
error: "Invalid workflow_id",
},
{
status: 500,
statusText: "Invalid workflow_id",
headers: corsHeaders,
},
);
}
// Case 2 update workflow
const { version: _version } = await createNewWorkflowVersion({
workflow_id: workflow_id,
workflowData: {
workflow,
workflow_api,
snapshot,
},
});
version = _version;
} else {
return c.json(
{
error: "Invalid request, missing either workflow_id or name",
},
{
status: 500,
statusText: "Invalid request",
headers: corsHeaders,
},
);
}
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return c.json(
{
error: errorMessage,
},
{
statusText: "Invalid request",
status: 500,
headers: corsHeaders,
},
);
}
return c.json(
{
workflow_id: workflow_id,
version: version,
},
{
status: 200,
headers: corsHeaders,
},
);
});
app.route("/upload-workflow").options(async (c) => {
return new Response(null, {
status: 204,
headers: corsHeaders,
});
});
};
+2 -3
View File
@@ -1,10 +1,9 @@
import { z } from "zod";
export const APIKeyBodyRequest = z.object({
user_id: z.string().optional().nullable(),
org_id: z.string().optional().nullable(),
user_id: z.string().optional(),
org_id: z.string().optional(),
iat: z.number(),
exp: z.number().optional(),
});
export type APIKeyUserType = z.infer<typeof APIKeyBodyRequest>;
+5
View File
@@ -0,0 +1,5 @@
import { insertCivitaiCheckpointSchema } from "@/db/schema";
export const addCivitaiCheckpointSchema = insertCivitaiCheckpointSchema.pick({
civitai_url: true,
});
+1 -1
View File
@@ -17,7 +17,7 @@ export const insertCustomMachineSchema = createInsertSchema(machinesTable, {
comfyui: "d0165d819afe76bd4e6bdd710eb5f3e571b6a804",
git_custom_nodes: {
"https://github.com/BennyKok/comfyui-deploy.git": {
hash: "43fe0a384aa5fa9e141d4a264b2ed40a73b817bc",
hash: "1e99474780951043e1b001e1877de3f2eb45cfba",
disabled: false,
},
},
+1 -41
View File
@@ -1,46 +1,6 @@
import { db } from "@/db/db";
import type { WorkflowVersionType } from "@/db/schema";
import { workflowTable, workflowVersionTable } from "@/db/schema";
import { eq, sql } from "drizzle-orm";
export async function createNewWorkflowVersion({
workflow_id,
workflowData,
}: {
workflow_id: string;
workflowData: Pick<
WorkflowVersionType,
"workflow" | "workflow_api" | "snapshot"
>;
}) {
// Add a new version
const data = await db
.insert(workflowVersionTable)
.values({
workflow_id,
...workflowData,
version: sql`(
SELECT COALESCE(MAX(version), 0) + 1
FROM ${workflowVersionTable}
WHERE workflow_id = ${workflow_id}
)`,
})
.returning();
const version = data[0].version;
// Touch up the last updated time
await db
.update(workflowTable)
.set({
updated_at: new Date(),
})
.where(eq(workflowTable.id, workflow_id))
.returning();
return {
version,
};
}
export async function createNewWorkflow({
workflow_name,
@@ -50,7 +10,7 @@ export async function createNewWorkflow({
}: {
workflow_name: string;
user_id: string;
org_id?: string | null;
org_id?: string;
workflowData: Pick<
WorkflowVersionType,
"workflow" | "workflow_api" | "snapshot"
+6 -20
View File
@@ -1,5 +1,6 @@
"use server";
import { withServerPromise } from "./withServerPromise";
import { db } from "@/db/db";
import type {
MachineType,
@@ -15,7 +16,6 @@ import { and, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import "server-only";
import { v4 } from "uuid";
import { withServerPromise } from "./withServerPromise";
export const createRun = withServerPromise(
async ({
@@ -38,7 +38,7 @@ export const createRun = withServerPromise(
? await db.query.machinesTable.findFirst({
where: and(
eq(machinesTable.id, machine_id),
eq(machinesTable.disabled, false),
eq(machinesTable.disabled, false)
),
})
: machine_id;
@@ -91,11 +91,6 @@ export const createRun = withServerPromise(
if (node.inputs["input_id"] === key) {
node.inputs["input_id"] = inputs[key];
}
// Fix for external text default value
if (node.class_type == "ComfyUIDeployExternalText") {
node.inputs["default_value"] = inputs[key];
}
});
}
}
@@ -148,7 +143,7 @@ export const createRun = withServerPromise(
throw new Error(
`Error creating run, ${
___result.statusText
} ${await ___result.text()}`,
} ${await ___result.text()}`
);
console.log(_data, ___result);
break;
@@ -182,7 +177,7 @@ export const createRun = withServerPromise(
throw new Error(
`Error creating run, ${
__result.statusText
} ${await __result.text()}`,
} ${await __result.text()}`
);
console.log(data, __result);
break;
@@ -204,7 +199,7 @@ export const createRun = withServerPromise(
let message = `Error creating run, ${_result.statusText}`;
try {
const result = await ComfyAPI_Run.parseAsync(
await _result.json(),
await _result.json()
);
message += ` ${result.node_errors}`;
} catch (error) {}
@@ -224,20 +219,11 @@ export const createRun = withServerPromise(
throw e;
}
// It successfully started, update the started_at time
await db
.update(workflowRunsTable)
.set({
started_at: new Date(),
})
.where(eq(workflowRunsTable.id, workflow_run[0].id));
return {
workflow_run_id: workflow_run[0].id,
message: "Successful workflow run",
};
},
}
);
export async function checkStatus(run_id: string) {
+12 -17
View File
@@ -1,28 +1,23 @@
"use server";
import { db } from "@/db/db";
import { apiKeyTable, authRequestsTable } from "@/db/schema";
import { withServerPromise } from "@/server/withServerPromise";
import { apiKeyTable } from "@/db/schema";
import { auth } from "@clerk/nextjs";
import { and, desc, eq, isNull } from "drizzle-orm";
import jwt from "jsonwebtoken";
import { revalidatePath } from "next/cache";
export const createAuthRequest = withServerPromise(
async (request_id: string) => {
const { userId, orgId } = auth();
// export const nanoid = customAlphabet(
// "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
// );
const result = await db.insert(authRequestsTable).values({
request_id: request_id,
user_id: userId,
org_id: orgId,
});
// const prefixes = {
// cd: "cd",
// } as const;
return {
message: "Auth request created, you may now return to your application.",
};
},
);
// function newId(prefix: keyof typeof prefixes): string {
// return [prefixes[prefix], nanoid(16)].join("_");
// }
export async function addNewAPIKey(name: string) {
const { userId, orgId } = auth();
@@ -34,7 +29,7 @@ export async function addNewAPIKey(name: string) {
if (orgId) {
token = jwt.sign(
{ user_id: userId, org_id: orgId },
process.env.JWT_SECRET!,
process.env.JWT_SECRET!
);
} else {
token = jwt.sign({ user_id: userId }, process.env.JWT_SECRET!);
@@ -98,7 +93,7 @@ export async function getAPIKeys() {
where: and(
eq(apiKeyTable.user_id, userId),
isNull(apiKeyTable.org_id),
eq(apiKeyTable.revoked, false),
eq(apiKeyTable.revoked, false)
),
orderBy: desc(apiKeyTable.created_at),
});
+224
View File
@@ -0,0 +1,224 @@
"use server";
import { auth } from "@clerk/nextjs";
import {
checkpointTable,
CheckpointType,
volumeTable,
CheckpointVolumeType,
} from "@/db/schema";
import { withServerPromise } from "./withServerPromise";
import { redirect } from "next/navigation";
import { db } from "@/db/db";
import type { z } from "zod";
import { headers } from "next/headers";
import { addCivitaiCheckpointSchema } from "./addCheckpointSchema";
import { and, eq, isNull } from "drizzle-orm";
import { CivitaiModelResponse } from "@/types/civitai";
export async function getCheckpoints() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const checkpoints = await db
.select()
.from(checkpointTable)
.where(
orgId
? eq(checkpointTable.org_id, orgId)
// make sure org_id is null
: and(
eq(checkpointTable.user_id, userId),
isNull(checkpointTable.org_id),
),
);
return checkpoints;
}
export async function getCheckpointById(id: string) {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const checkpoint = await db
.select()
.from(checkpointTable)
.where(
and(
orgId ? eq(checkpointTable.org_id, orgId) : and(
eq(checkpointTable.user_id, userId),
isNull(checkpointTable.org_id),
),
eq(checkpointTable.id, id),
),
);
return checkpoint[0];
}
export async function getCheckpointVolumes() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const checkpointVolume = await db
.select()
.from(volumeTable)
.where(
and(
orgId
? eq(volumeTable.org_id, orgId)
// make sure org_id is null
: and(
eq(volumeTable.user_id, userId),
isNull(volumeTable.org_id),
),
eq(volumeTable.disabled, false),
),
);
return checkpointVolume;
}
export async function addCheckpointVolume() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
// Insert the new volume into the checkpointVolumeTable
const insertedVolume = await db
.insert(volumeTable)
.values({
user_id: userId,
org_id: orgId,
volume_name: `checkpoints_${userId}`,
// created_at and updated_at will be set to current timestamp by default
disabled: false, // Default value
})
.returning(); // Returns the inserted row
return insertedVolume;
}
function getUrl(civitai_url: string) {
// expect to be a URL to be https://civitai.com/models/36520
// possiblity with slugged name and query-param modelVersionId
const baseUrl = "https://civitai.com/api/v1/models/";
const url = new URL(civitai_url);
const pathSegments = url.pathname.split("/");
const modelId = pathSegments[pathSegments.indexOf("models") + 1];
const modelVersionId = url.searchParams.get("modelVersionId");
return { url: baseUrl + modelId, modelVersionId };
}
export const addCivitaiCheckpoint = withServerPromise(
async (data: z.infer<typeof addCivitaiCheckpointSchema>) => {
const { userId, orgId } = auth();
if (!data.civitai_url) return { error: "no civitai_url" };
if (!userId) return { error: "No user id" };
const { url, modelVersionId } = getUrl(data?.civitai_url);
const civitaiModelRes = await fetch(url)
.then((x) => x.json())
.then((a) => {
return CivitaiModelResponse.parse(a);
});
if (civitaiModelRes?.modelVersions?.length === 0) {
return; // no versions to download
}
let selectedModelVersion;
let selectedModelVersionId: string | null = modelVersionId;
if (!selectedModelVersionId) {
selectedModelVersion = civitaiModelRes.modelVersions[0];
selectedModelVersionId = civitaiModelRes.modelVersions[0].id.toString();
} else {
selectedModelVersion = civitaiModelRes.modelVersions.find((version) =>
version.id.toString() === selectedModelVersionId
);
if (!selectedModelVersion) {
return; // version id is wrong
}
selectedModelVersionId = selectedModelVersion?.id.toString();
}
const checkpointVolumes = await getCheckpointVolumes();
let cVolume;
if (checkpointVolumes.length === 0) {
const volume = await addCheckpointVolume();
cVolume = volume[0];
} else {
cVolume = checkpointVolumes[0];
}
const a = await db
.insert(checkpointTable)
.values({
user_id: userId,
org_id: orgId,
upload_type: "civitai",
civitai_id: civitaiModelRes.id.toString(),
civitai_version_id: selectedModelVersionId,
civitai_url: data.civitai_url,
civitai_download_url: selectedModelVersion.downloadUrl,
civitai_model_response: civitaiModelRes,
checkpoint_volume_id: cVolume.id,
})
.returning();
const b = a[0];
await uploadCheckpoint(data, b, cVolume);
redirect(`/checkpoints/${b.id}`);
},
);
async function uploadCheckpoint(
data: z.infer<typeof addCivitaiCheckpointSchema>,
b: CheckpointType,
v: CheckpointVolumeType,
) {
const headersList = headers();
const domain = headersList.get("x-forwarded-host") || "";
const protocol = headersList.get("x-forwarded-proto") || "";
if (domain === "") {
throw new Error("No domain");
}
// Call remote builder
const result = await fetch(
`${process.env.MODAL_BUILDER_URL!}/upload_volume`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
download_url: data.civitai_url,
volume_name: v.volume_name,
volume_id: v.id,
callback_url: `${protocol}://${domain}/api/volume-updated`,
}),
},
);
if (!result.ok) {
const error_log = await result.text();
await db
.update(checkpointTable)
.set({
...data,
status: "error",
build_log: error_log,
})
.where(eq(checkpointTable.id, b.id));
throw new Error(`Error: ${result.statusText} ${error_log}`);
} else {
// setting the build machine id
const json = await result.json();
await db
.update(checkpointTable)
.set({
...data,
upload_machine_id: json.build_machine_instance_id,
})
.where(eq(checkpointTable.id, b.id));
}
}
+22 -56
View File
@@ -7,19 +7,17 @@ import { createNewWorkflow } from "@/server/createNewWorkflow";
import { addCustomMachine } from "@/server/curdMachine";
import { withServerPromise } from "@/server/withServerPromise";
import { auth } from "@clerk/nextjs";
import { clerkClient } from "@clerk/nextjs/server";
import slugify from "@sindresorhus/slugify";
import { and, eq, isNull, or } from "drizzle-orm";
import { and, eq, isNull } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import "server-only";
import { validate as isValidUUID } from "uuid";
import type { z } from "zod";
export async function createDeployments(
workflow_id: string,
version_id: string,
machine_id: string,
environment: DeploymentType["environment"],
environment: DeploymentType["environment"]
) {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
@@ -32,7 +30,7 @@ export async function createDeployments(
const existingDeployment = await db.query.deploymentsTable.findFirst({
where: and(
eq(deploymentsTable.workflow_id, workflow_id),
eq(deploymentsTable.environment, environment),
eq(deploymentsTable.environment, environment)
),
});
@@ -47,27 +45,6 @@ export async function createDeployments(
})
.where(eq(deploymentsTable.id, existingDeployment.id));
} else {
const workflow = await db.query.workflowTable.findFirst({
where: eq(workflowTable.id, workflow_id),
with: {
user: {
columns: {
name: true,
},
},
},
});
if (!workflow) throw new Error("No workflow found");
const userName = workflow.org_id
? await clerkClient.organizations
.getOrganization({
organizationId: workflow.org_id,
})
.then((x) => x.name)
: workflow.user.name;
await db.insert(deploymentsTable).values({
user_id: userId,
workflow_id,
@@ -75,8 +52,6 @@ export async function createDeployments(
machine_id,
environment,
org_id: orgId,
// only create share slug if this is public share
share_slug: environment == "public-share" ? slugify(`${userName} ${workflow.name}`) : null
});
}
revalidatePath(`/${workflow_id}`);
@@ -93,7 +68,7 @@ export async function findAllDeployments() {
where: and(
orgId
? eq(workflowTable.org_id, orgId)
: and(eq(workflowTable.user_id, userId), isNull(workflowTable.org_id)),
: and(eq(workflowTable.user_id, userId), isNull(workflowTable.org_id))
),
columns: {
name: true,
@@ -122,9 +97,7 @@ export async function findSharedDeployment(workflow_id: string) {
const deploymentData = await db.query.deploymentsTable.findFirst({
where: and(
eq(deploymentsTable.environment, "public-share"),
isValidUUID(workflow_id)
? eq(deploymentsTable.id, workflow_id)
: eq(deploymentsTable.share_slug, workflow_id),
eq(deploymentsTable.id, workflow_id)
),
with: {
user: true,
@@ -145,20 +118,15 @@ export async function findSharedDeployment(workflow_id: string) {
export const removePublicShareDeployment = withServerPromise(
async (deployment_id: string) => {
const [removed] = await db
await db
.delete(deploymentsTable)
.where(
and(
eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, deployment_id),
),
)
.returning();
// revalidatePath(
// `/workflows/${removed.workflow_id}`
// )
},
eq(deploymentsTable.id, deployment_id)
)
);
}
);
export const cloneWorkflow = withServerPromise(
@@ -166,7 +134,7 @@ export const cloneWorkflow = withServerPromise(
const deployment = await db.query.deploymentsTable.findFirst({
where: and(
eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, deployment_id),
eq(deploymentsTable.id, deployment_id)
),
with: {
version: true,
@@ -196,14 +164,14 @@ export const cloneWorkflow = withServerPromise(
return {
message: "Successfully cloned workflow",
};
},
}
);
export const cloneMachine = withServerPromise(async (deployment_id: string) => {
const deployment = await db.query.deploymentsTable.findFirst({
where: and(
eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, deployment_id),
eq(deploymentsTable.id, deployment_id)
),
with: {
machine: true,
@@ -241,17 +209,15 @@ export async function findUserShareDeployment(share_id: string) {
.from(deploymentsTable)
.where(
and(
isValidUUID(share_id)
? eq(deploymentsTable.id, share_id)
: eq(deploymentsTable.share_slug, share_id),
eq(deploymentsTable.id, share_id),
eq(deploymentsTable.environment, "public-share"),
orgId
? eq(deploymentsTable.org_id, orgId)
: and(
eq(deploymentsTable.user_id, userId),
isNull(deploymentsTable.org_id),
),
),
eq(deploymentsTable.user_id, userId),
isNull(deploymentsTable.org_id)
)
)
);
if (!deployment) throw new Error("No deployment found");
@@ -277,11 +243,11 @@ export const updateSharePageInfo = withServerPromise(
.where(
and(
eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, id),
),
eq(deploymentsTable.id, id)
)
)
.returning();
return { message: "Info Updated" };
},
}
);
-49
View File
@@ -1,49 +0,0 @@
"use server";
import { getMachineById } from "@/server/curdMachine";
import { auth } from "@clerk/nextjs";
import jwt from "jsonwebtoken";
import { getOrgOrUserDisplayName } from "@/server/getOrgOrUserDisplayName";
import { withServerPromise } from "@/server/withServerPromise";
import "server-only";
import { headers } from "next/headers";
export const editWorkflowOnMachine = withServerPromise(
async (workflow_version_id: string, machine_id: string) => {
const { userId, orgId } = auth();
const headersList = headers();
const host = headersList.get("host") || "";
const protocol = headersList.get("x-forwarded-proto") || "";
const domain = `${protocol}://${host}`;
if (!userId) {
throw new Error("No user id");
}
const machine = await getMachineById(machine_id);
const expireTime = "1w";
const token = jwt.sign(
{ user_id: userId, org_id: orgId },
process.env.JWT_SECRET!,
{
expiresIn: expireTime,
},
);
const userName = await getOrgOrUserDisplayName(orgId, userId);
let endpoint = machine.endpoint;
if (machine.type === "comfy-deploy-serverless") {
endpoint = machine.endpoint.replace("comfyui-api", "comfyui-app");
}
return `${endpoint}?workflow_version_id=${encodeURIComponent(
workflow_version_id,
)}&auth_token=${encodeURIComponent(token)}&org_display=${encodeURIComponent(
userName,
)}&origin=${encodeURIComponent(domain)}`;
},
);
+26 -36
View File
@@ -16,42 +16,32 @@ export async function findAllRuns({
offset = 0,
}: RunsSearchTypes) {
return await db.query.workflowRunsTable.findMany({
where: eq(workflowRunsTable.workflow_id, workflow_id),
orderBy: desc(workflowRunsTable.created_at),
offset: offset,
limit: limit,
extras: {
number: sql<number>`row_number() over (order by created_at)`.as(
"number",
),
total: sql<number>`count(*) over ()`.as("total"),
duration:
sql<number>`(extract(epoch from ended_at) - extract(epoch from created_at))`.as(
"duration",
),
cold_start_duration:
sql<number>`(extract(epoch from started_at) - extract(epoch from created_at))`.as(
"cold_start_duration",
),
run_duration:
sql<number>`(extract(epoch from ended_at) - extract(epoch from started_at))`.as(
"run_duration",
),
},
with: {
machine: {
columns: {
name: true,
endpoint: true,
},
},
version: {
columns: {
version: true,
},
},
},
});
where: eq(workflowRunsTable.workflow_id, workflow_id),
orderBy: desc(workflowRunsTable.created_at),
offset: offset,
limit: limit,
extras: {
number: sql<number>`row_number() over (order by created_at)`.as("number"),
total: sql<number>`count(*) over ()`.as("total"),
duration:
sql<number>`(extract(epoch from ended_at) - extract(epoch from created_at))`.as(
"duration"
),
},
with: {
machine: {
columns: {
name: true,
endpoint: true,
},
},
version: {
columns: {
version: true,
},
},
},
});
}
export async function findAllRunsWithCounts(props: RunsSearchTypes) {
+39
View File
@@ -0,0 +1,39 @@
import { db } from "@/db/db";
import {
checkpointTable,
} from "@/db/schema";
import { auth } from "@clerk/nextjs";
import { and, desc, eq, isNull } from "drizzle-orm";
export async function getAllUserCheckpoints() {
const { userId, orgId } = await auth();
if (!userId) {
return null;
}
const checkpoints = await db.query.checkpointTable.findMany({
with: {
user: {
columns: {
name: true,
},
},
},
columns: {
id: true,
updated_at: true,
name: true,
civitai_url: true,
civitai_model_response: true,
is_public: true,
},
orderBy: desc(checkpointTable.updated_at),
where:
orgId != undefined
? eq(checkpointTable.org_id, orgId)
: and(eq(checkpointTable.user_id, userId), isNull(checkpointTable.org_id)),
});
return checkpoints;
}
@@ -4,10 +4,8 @@ import {
workflowTable,
workflowVersionTable,
} from "@/db/schema";
import { APIKeyUserType } from "@/server/APIKeyBodyRequest";
import { auth } from "@clerk/nextjs";
import { and, desc, eq, isNull } from "drizzle-orm";
import { redirect } from "next/navigation";
export async function getAllUserWorkflow() {
const { userId, orgId } = await auth();
@@ -53,29 +51,3 @@ export async function getAllUserWorkflow() {
return workflow;
}
export async function getWorkflowVersion(
apiUser: APIKeyUserType,
version_id: string,
) {
const { org_id, user_id } = apiUser;
if (!user_id) {
throw new Error("No user id");
}
const parentWorkflow = await db.query.workflowTable.findFirst({
where:
org_id != undefined
? eq(workflowTable.org_id, org_id)
: and(eq(workflowTable.user_id, user_id), isNull(workflowTable.org_id)),
});
if (!parentWorkflow) {
throw new Error("No workflow found");
}
return db.query.workflowVersionTable.findFirst({
where: eq(workflowVersionTable.id, version_id),
});
}
@@ -1,18 +0,0 @@
import { db } from "@/db/db";
import { usersTable } from "@/db/schema";
import { clerkClient } from "@clerk/nextjs";
import { eq } from "drizzle-orm";
export async function getOrgOrUserDisplayName(
orgId: string | undefined | null,
userId: string,
) {
return orgId
? await clerkClient.organizations
.getOrganization({
organizationId: orgId,
})
.then((x) => x.name)
: (await db.select().from(usersTable).where(eq(usersTable.id, userId)))[0]
.name;
}
+15 -17
View File
@@ -49,26 +49,24 @@ export async function getRunsData(run_id: string, user?: APIKeyUserType) {
for (let i = 0; i < data.outputs.length; i++) {
const output = data.outputs[i];
if (output.data?.images !== undefined)
replaceUrls(output.data?.images, data.id);
if (output.data?.files !== undefined)
replaceUrls(output.data?.files, data.id);
if (output.data?.gifs !== undefined)
replaceUrls(output.data?.gifs, data.id);
if (output.data?.images !== undefined) {
for (let j = 0; j < output.data?.images.length; j++) {
const element = output.data?.images[j];
element.url = replaceCDNUrl(
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${data.id}/${element.filename}`
);
}
} else if (output.data?.files !== undefined) {
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;
}
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}`,
);
}
}
+126
View File
@@ -0,0 +1,126 @@
import { z } from 'zod';
// from chatgpt https://chat.openai.com/share/4985d20b-30b1-4a28-87f6-6ebf84a1040e
export const creatorSchema = z.object({
username: z.string().optional(),
image: z.string().url().optional(),
});
export const fileMetadataSchema = z.object({
fp: z.string().optional(),
size: z.string().optional(),
format: z.string().optional(),
});
export const fileSchema = z.object({
id: z.number(),
sizeKB: z.number().optional(),
name: z.string(),
type: z.string().optional(),
metadata: fileMetadataSchema.optional(),
pickleScanResult: z.string().optional(),
pickleScanMessage: z.string().nullable(),
virusScanResult: z.string().optional(),
virusScanMessage: z.string().nullable(),
scannedAt: z.string().optional(),
hashes: z.record(z.string()).optional(),
downloadUrl: z.string().url(),
primary: z.boolean().optional().optional(),
});
export const imageMetadataSchema = z.object({
hash: z.string(),
width: z.number(),
height: z.number(),
});
export const imageMetaSchema = z.object({
ENSD: z.string().optional(),
Size: z.string().optional(),
seed: z.number().optional(),
Model: z.string().optional(),
steps: z.number().optional(),
hashes: z.record(z.string()).optional(),
prompt: z.string().optional(),
sampler: z.string().optional(),
cfgScale: z.number().optional(),
ClipSkip: z.number().optional(),
resources: z.array(
z.object({
hash: z.string().optional(),
name: z.string(),
type: z.string(),
weight: z.number().optional(),
})
).optional(),
ModelHash: z.string().optional(),
HiresSteps: z.string().optional(),
HiresUpscale: z.string().optional(),
HiresUpscaler: z.string().optional(),
negativePrompt: z.string(),
DenoisingStrength: z.number().optional(),
});
export const imageSchema = z.object({
url: z.string().url().optional(),
nsfw: z.enum(["None", "Soft", "Mature"]).optional(),
width: z.number().optional(),
height: z.number().optional(),
hash: z.string().optional(),
type: z.string().optional(),
metadata: imageMetadataSchema.optional(),
meta: imageMetaSchema.optional(),
});
export const modelVersionSchema = z.object({
id: z.number(),
modelId: z.number(),
name: z.string(),
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
status: z.enum(["Published", "Unpublished"]).optional(),
publishedAt: z.string().optional(),
trainedWords: z.array(z.string()).nullable(),
trainingStatus: z.string().nullable(),
trainingDetails: z.string().nullable(),
baseModel: z.string().optional(),
baseModelType: z.string().optional(),
earlyAccessTimeFrame: z.number().optional(),
description: z.string().nullable(),
vaeId: z.string().nullable(),
stats: z.object({
downloadCount: z.number(),
ratingCount: z.number(),
rating: z.number(),
}).optional(),
files: z.array(fileSchema),
images: z.array(imageSchema),
downloadUrl: z.string().url(),
});
export const statsSchema = z.object({
downloadCount: z.number(),
favoriteCount: z.number(),
commentCount: z.number(),
ratingCount: z.number(),
rating: z.number(),
tippedAmountCount: z.number(),
});
export const CivitaiModelResponse = z.object({
id: z.number(),
name: z.string().optional(),
description: z.string().optional(),
type: z.enum(["Checkpoint", "Lora"]),
poi: z.boolean().optional(),
nsfw: z.boolean().optional(),
allowNoCredit: z.boolean().optional(),
allowCommercialUse: z.enum(["Rent"]).optional(),
allowDerivatives: z.boolean().optional(),
allowDifferentLicense: z.boolean().optional(),
stats: statsSchema.optional(),
creator: creatorSchema.optional(),
tags: z.array(z.string()).optional(),
modelVersions: z.array(modelVersionSchema),
});