Compare commits

..
5 changed files with 939 additions and 1134 deletions
+5 -19
View File
@@ -4,15 +4,12 @@ import numpy as np
import torch
import folder_paths
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyUIDeployExternalLora:
@classmethod
def INPUT_TYPES(s):
@@ -25,10 +22,6 @@ class ComfyUIDeployExternalLora:
},
"optional": {
"default_lora_name": (folder_paths.get_filename_list("loras"),),
"lora_save_name": ( # if `default_lora_name` is a link to download a file, we will attempt to save it with this name
"STRING",
{"multiline": False, "default": ""},
),
},
}
@@ -39,24 +32,17 @@ class ComfyUIDeployExternalLora:
CATEGORY = "deploy"
def run(self, input_id, default_lora_name=None, lora_save_name=None):
def run(self, input_id, default_lora_name=None):
import requests
import os
import uuid
if default_lora_name.startswith("http"):
if lora_save_name:
existing_loras = folder_paths.get_filename_list("loras")
# Check if lora_save_name exists in the list
if lora_save_name in existing_loras:
print(f"using lora: {lora_save_name}")
return (lora_save_name,)
else:
lora_save_name = str(uuid.uuid4()) + ".safetensors"
print(lora_save_name)
unique_filename = str(uuid.uuid4()) + ".safetensors"
print(unique_filename)
print(folder_paths.folder_names_and_paths["loras"][0][0])
destination_path = os.path.join(
folder_paths.folder_names_and_paths["loras"][0][0], lora_save_name
folder_paths.folder_names_and_paths["loras"][0][0], unique_filename
)
print(destination_path)
print("Downloading external lora - " + input_id + " to " + destination_path)
@@ -67,7 +53,7 @@ class ComfyUIDeployExternalLora:
)
with open(destination_path, "wb") as out_file:
out_file.write(response.content)
return (lora_save_name,)
return (unique_filename,)
else:
print(f"using lora: {default_lora_name}")
return (default_lora_name,)
-43
View File
@@ -1,43 +0,0 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
import json
class ComfyUIDeployExternalTextList:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": 'input_text_list'},
),
"text": (
"STRING",
{"multiline": True, "default": "[]"},
),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "run"
CATEGORY = "text"
def run(self, input_id, text=None):
text_list = []
try:
text_list = json.loads(text) # Assuming text is a JSON array string
except Exception as e:
print(f"Error processing images: {e}")
pass
return [text_list]
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalTextList": ComfyUIDeployExternalTextList}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalTextList": "External Text List (ComfyUI Deploy)"}
+5 -6
View File
@@ -108,8 +108,7 @@ def log_span(name):
if use_logfire:
with logger.span(name):
yield
else:
yield
# else:
# logger.info(f"Start: {name}")
# yield
# logger.info(f"End: {name}")
@@ -349,7 +348,7 @@ async def comfy_deploy_run(request):
status = 200
if "node_errors" in res and res["node_errors"] is not None:
if "node_errors" in res and res["node_errors"]:
# Even tho there are node_errors it can still be run
status = 400
await update_run_with_output(prompt_id, {
@@ -387,7 +386,7 @@ async def stream_prompt(data):
workflow_api=workflow_api
)
# log('info', "Begin prompt", prompt=prompt)
log('info', "Begin prompt", prompt=prompt)
try:
res = post_prompt(prompt)
@@ -410,7 +409,7 @@ async def stream_prompt(data):
status = 200
if "node_errors" in res and res["node_errors"] is not None:
if "node_errors" in res and res["node_errors"]:
# Even tho there are node_errors it can still be run
status = 400
await update_run_with_output(prompt_id, {
@@ -454,7 +453,7 @@ async def stream_response(request):
if not comfy_message_queues[prompt_id].empty():
data = await comfy_message_queues[prompt_id].get()
# log('info', data["event"], data=json.dumps(data))
log('info', data["event"], data=json.dumps(data))
# logger.info("listener", data)
await response.write(f"event: event_update\ndata: {json.dumps(data)}\n\n".encode('utf-8'))
await response.drain() # Ensure the buffer is flushed
+57 -192
View File
@@ -24,10 +24,7 @@ function dispatchAPIEventData(data) {
message += "\n" + nodeError.class_type + ":";
for (const errorReason of nodeError.errors) {
message +=
"\n - " +
errorReason.message +
": " +
errorReason.details;
"\n - " + errorReason.message + ": " + errorReason.details;
}
}
@@ -47,38 +44,32 @@ function dispatchAPIEventData(data) {
// window.name = this.clientId; // use window name so it isnt reused when duplicating tabs
// sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow
}
api.dispatchEvent(
new CustomEvent("status", { detail: msg.data.status })
);
api.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
break;
case "progress":
api.dispatchEvent(
new CustomEvent("progress", { detail: msg.data })
);
api.dispatchEvent(new CustomEvent("progress", { detail: msg.data }));
break;
case "executing":
api.dispatchEvent(
new CustomEvent("executing", { detail: msg.data.node })
new CustomEvent("executing", { detail: msg.data.node }),
);
break;
case "executed":
api.dispatchEvent(
new CustomEvent("executed", { detail: msg.data })
);
api.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
break;
case "execution_start":
api.dispatchEvent(
new CustomEvent("execution_start", { detail: msg.data })
new CustomEvent("execution_start", { detail: msg.data }),
);
break;
case "execution_error":
api.dispatchEvent(
new CustomEvent("execution_error", { detail: msg.data })
new CustomEvent("execution_error", { detail: msg.data }),
);
break;
case "execution_cached":
api.dispatchEvent(
new CustomEvent("execution_cached", { detail: msg.data })
new CustomEvent("execution_cached", { detail: msg.data }),
);
break;
default:
@@ -152,13 +143,11 @@ const ext = {
}
if (!workflow_version_id) {
console.error(
"No workflow_version_id provided in query parameters."
);
console.error("No workflow_version_id provided in query parameters.");
} else {
loadingDialog.showLoading(
"Loading workflow from " + org_display,
"Please wait..."
"Please wait...",
);
fetch(endpoint + "/api/workflow-version/" + workflow_version_id, {
method: "GET",
@@ -171,10 +160,7 @@ const ext = {
const data = await res.json();
const { workflow, workflow_id, error } = data;
if (error) {
infoDialog.showMessage(
"Unable to load this workflow",
error
);
infoDialog.showMessage("Unable to load this workflow", error);
return;
}
@@ -197,7 +183,7 @@ const ext = {
window.history.replaceState(
{},
document.title,
window.location.pathname
window.location.pathname,
);
});
}
@@ -220,37 +206,22 @@ const ext = {
ComfyWidgets.STRING(
this,
"workflow_name",
[
"",
{
default: this.properties.workflow_name,
multiline: false,
},
],
app
["", { default: this.properties.workflow_name, multiline: false }],
app,
);
ComfyWidgets.STRING(
this,
"workflow_id",
[
"",
{
default: this.properties.workflow_id,
multiline: false,
},
],
app
["", { default: this.properties.workflow_id, multiline: false }],
app,
);
ComfyWidgets.STRING(
this,
"version",
[
"",
{ default: this.properties.version, multiline: false },
],
app
["", { default: this.properties.version, multiline: false }],
app,
);
// this.widgets.forEach((w) => {
@@ -277,7 +248,7 @@ const ext = {
title_mode: LiteGraph.NORMAL_TITLE,
title: "Comfy Deploy",
collapsable: true,
})
}),
);
ComfyDeploy.category = "deploy";
@@ -307,13 +278,7 @@ const ext = {
sendEventToCD("cd_plugin_onDeployChanges", prompt);
} else if (message.type === "queue_prompt") {
const prompt = await app.graphToPrompt();
if (typeof api.handlePromptGenerated === "function") {
api.handlePromptGenerated(prompt);
} else {
console.warn(
"api.handlePromptGenerated is not a function"
);
}
sendEventToCD("cd_plugin_onQueuePrompt", prompt);
} else if (message.type === "get_prompt") {
const prompt = await app.graphToPrompt();
@@ -336,72 +301,6 @@ const ext = {
app.graph.add(node);
app.graph.afterChange();
} else if (message.type === "zoom_to_node") {
const nodeId = message.data.nodeId;
const position = message.data.position;
const node = app.graph.getNodeById(nodeId);
if (!node) return;
const canvas = app.canvas;
const targetScale = 1;
const targetOffsetX =
canvas.canvas.width / 4 -
position[0] -
node.size[0] / 2;
const targetOffsetY =
canvas.canvas.height / 4 -
position[1] -
node.size[1] / 2;
const startScale = canvas.ds.scale;
const startOffsetX = canvas.ds.offset[0];
const startOffsetY = canvas.ds.offset[1];
const duration = 400; // Animation duration in milliseconds
const startTime = Date.now();
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function lerp(start, end, t) {
return start * (1 - t) + end * t;
}
function animate() {
const currentTime = Date.now();
const elapsedTime = currentTime - startTime;
const t = Math.min(elapsedTime / duration, 1);
const easedT = easeOutCubic(t);
const currentScale = lerp(
startScale,
targetScale,
easedT
);
const currentOffsetX = lerp(
startOffsetX,
targetOffsetX,
easedT
);
const currentOffsetY = lerp(
startOffsetY,
targetOffsetY,
easedT
);
canvas.setZoom(currentScale);
canvas.ds.offset = [currentOffsetX, currentOffsetY];
canvas.draw(true, true);
if (t < 1) {
requestAnimationFrame(animate);
}
}
animate();
}
// else if (message.type === "refresh") {
// sendEventToCD("cd_plugin_onRefresh");
@@ -442,7 +341,7 @@ const ext = {
function showError(title, message) {
infoDialog.show(
`<h3 style="margin: 0px; color: red;">${title}</h3><br><span>${message}</span> `
`<h3 style="margin: 0px; color: red;">${title}</h3><br><span>${message}</span> `,
);
}
@@ -476,14 +375,11 @@ function createDynamicUIHtml(data) {
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 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
${node.warning
? `<p style="font-size: 14px; color: #d69e2e;">${node.warning}</p>`
: ""
}
@@ -500,8 +396,7 @@ function createDynamicUIHtml(data) {
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 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>`;
@@ -518,8 +413,7 @@ function createDynamicUIHtml(data) {
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 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>`;
@@ -550,7 +444,7 @@ async function deployWorkflow() {
if (deployMeta.length == 0) {
const text = await inputDialog.input(
"Create your deployment",
"Workflow name"
"Workflow name",
);
if (!text) return;
console.log(text);
@@ -591,7 +485,7 @@ async function deployWorkflow() {
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label>
</div>
`
`,
);
if (!ok) return;
@@ -610,7 +504,7 @@ async function deployWorkflow() {
if (!snapshot) {
showError(
"Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager"
"Unable to generate snapshot, please install ComfyUI Manager",
);
return;
}
@@ -631,7 +525,7 @@ async function deployWorkflow() {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
}
},
)
.then((x) => x.json())
.catch(() => {
@@ -650,7 +544,7 @@ async function deployWorkflow() {
// Match previous hash for models
if (reuseHash && existing_workflow?.dependencies?.models) {
const previousModelHash = Object.entries(
existing_workflow?.dependencies?.models
existing_workflow?.dependencies?.models,
).flatMap(([key, value]) => {
return Object.values(value).map((x) => ({
...x,
@@ -672,9 +566,7 @@ async function deployWorkflow() {
console.log(file);
loadingDialog.showLoading("Generating hash", file);
const hash = await fetch(
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(
file
)}`
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(file)}`,
).then((x) => x.json());
loadingDialog.showLoading("Generating hash", file);
console.log(hash);
@@ -684,24 +576,18 @@ async function deployWorkflow() {
console.log("Uploading ", file);
loadingDialog.showLoading("Uploading file", file);
try {
const { download_url } = await fetch(
`/comfyui-deploy/upload-file`,
{
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
);
confirmDialog.confirm("Error", "Unable to upload file " + file);
});
loadingDialog.showLoading("Uploaded file", file);
console.log(download_url);
@@ -738,8 +624,8 @@ async function deployWorkflow() {
<iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
JSON.stringify(deps)
)}" />`
JSON.stringify(deps),
)}" />`,
// createDynamicUIHtml(deps),
);
if (!depsOk) return;
@@ -800,7 +686,7 @@ async function deployWorkflow() {
graph.change();
infoDialog.show(
`<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`
`<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`,
);
setTimeout(() => {
@@ -951,11 +837,9 @@ export class LoadingDialog extends ComfyDialog {
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 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${this.loadingIcon
}</h3>
${
message
${message
? `<label style="max-width: 100%; white-space: pre-wrap; word-wrap: break-word;">${message}</label>`
: ""
}
@@ -997,22 +881,17 @@ export class InputDialog extends InfoDialog {
type: "button",
textContent: "Save",
onclick: () => {
const input =
this.textElement.querySelector("#input").value;
const input = this.textElement.querySelector("#input").value;
if (input.trim() === "") {
showError(
"Input validation",
"Input cannot be empty"
);
showError("Input validation", "Input cannot be empty");
} else {
this.callback?.(input);
this.close();
this.textElement.querySelector("#input").value =
"";
this.textElement.querySelector("#input").value = "";
}
},
}),
]
],
),
];
}
@@ -1073,7 +952,7 @@ export class ConfirmDialog extends InfoDialog {
this.close();
},
}),
]
],
),
];
}
@@ -1130,7 +1009,7 @@ function getData(environment) {
function saveData(data) {
localStorage.setItem(
"comfy_deploy_env_data_" + data.environment,
JSON.stringify(data)
JSON.stringify(data),
);
}
@@ -1145,9 +1024,7 @@ export class ConfigDialog extends ComfyDialog {
this.element.style.paddingBottom = "20px";
this.container = document.createElement("div");
this.element
.querySelector(".comfy-modal-content")
.prepend(this.container);
this.element.querySelector(".comfy-modal-content").prepend(this.container);
}
createButtons() {
@@ -1181,7 +1058,7 @@ export class ConfigDialog extends ComfyDialog {
this.close();
},
}),
]
],
),
];
}
@@ -1193,8 +1070,7 @@ export class ConfigDialog extends ComfyDialog {
}
save(api_key, displayName) {
const deployOption =
this.container.querySelector("#deployOption").value;
const deployOption = this.container.querySelector("#deployOption").value;
localStorage.setItem("comfy_deploy_env", deployOption);
const endpoint = this.container.querySelector("#endpoint").value;
@@ -1226,32 +1102,22 @@ 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%;">
Endpoint:
<input id="endpoint" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="text" value="${
data.endpoint
<input id="endpoint" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="text" value="${data.endpoint
}">
</label>
<div style="color: white;">
API Key: User / Org <button style="font-size: 18px;">${
data.displayName ?? ""
API Key: User / Org <button style="font-size: 18px;">${data.displayName ?? ""
}</button>
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${
data.apiKey
<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"
${data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
}
</button>
</div>
@@ -1272,7 +1138,7 @@ export class ConfigDialog extends ComfyDialog {
clearInterval(poll);
infoDialog.showMessage(
"Timeout",
"Wait too long for the response, please try re-login"
"Wait too long for the response, please try re-login",
);
}, 30000); // Stop polling after 30 seconds
@@ -1283,15 +1149,14 @@ export class ConfigDialog extends ComfyDialog {
if (json.api_key) {
this.save(json.api_key, json.name);
this.close();
this.container.querySelector("#apiKey").value =
json.api_key;
this.container.querySelector("#apiKey").value = json.api_key;
// infoDialog.show();
clearInterval(this.poll);
clearTimeout(this.timeout);
// Refresh dialog
const a = await confirmDialog.confirm(
"Authenticated",
`<div>You will be able to upload workflow to <button style="font-size: 18px; width: fit;">${json.name}</button></div>`
`<div>You will be able to upload workflow to <button style="font-size: 18px; width: fit;">${json.name}</button></div>`,
);
configDialog.show();
}
+1 -3
View File
@@ -51,9 +51,7 @@ const createRunRoute = createRoute({
export const registerCreateRunRoute = (app: App) => {
app.openapi(createRunRoute, async (c) => {
const data = c.req.valid("json");
const proto = c.req.headers.get('x-forwarded-proto') || "http";
const host = c.req.headers.get('x-forwarded-host') || c.req.headers.get('host');
const origin = `${proto}://${host}` || new URL(c.req.url).origin;
const origin = new URL(c.req.url).origin;
const apiKeyTokenData = c.get("apiKeyTokenData")!;
const { deployment_id, inputs } = data;