feat: support new frontend!

This commit is contained in:
bennykok 2024-08-14 11:09:58 -07:00
parent f6ea252652
commit 10268825d9
3 changed files with 1203 additions and 1041 deletions

View File

@ -17,12 +17,13 @@ from urllib.parse import quote
import threading
import hashlib
import aiohttp
from aiohttp import ClientSession, web
import aiofiles
from typing import Dict, List, Union, Any, Optional
from PIL import Image
import copy
import struct
from aiohttp import ClientError
from aiohttp import web, ClientSession, ClientError
import atexit
# Global session
@ -836,6 +837,50 @@ async def send(event, data, sid=None):
logger.info(f"Exception: {e}")
traceback.print_exc()
@server.PromptServer.instance.routes.get('/comfydeploy/{tail:.*}')
@server.PromptServer.instance.routes.post('/comfydeploy/{tail:.*}')
async def proxy_to_comfydeploy(request):
# Get the base URL
base_url = f'https://www.comfydeploy.com/{request.match_info["tail"]}'
# Get all query parameters
query_params = request.query_string
# Construct the full target URL with query parameters
target_url = f"{base_url}?{query_params}" if query_params else base_url
print(f"Proxying request to: {target_url}")
try:
# Create a new ClientSession for each request
async with ClientSession() as client_session:
# Forward the request
client_req = await client_session.request(
method=request.method,
url=target_url,
headers={k: v for k, v in request.headers.items() if k.lower() not in ('host', 'content-length')},
data=await request.read(),
allow_redirects=False,
)
# Read the entire response content
content = await client_req.read()
# Try to decode the content as JSON
try:
json_data = json.loads(content)
# If successful, return a JSON response
return web.json_response(json_data, status=client_req.status)
except json.JSONDecodeError:
# If it's not valid JSON, return the content as-is
return web.Response(body=content, status=client_req.status, headers=client_req.headers)
except ClientError as e:
print(f"Client error occurred while proxying request: {str(e)}")
return web.Response(status=502, text=f"Bad Gateway: {str(e)}")
except Exception as e:
print(f"Error occurred while proxying request: {str(e)}")
return web.Response(status=500, text=f"Internal Server Error: {str(e)}")
prompt_server = server.PromptServer.instance

View File

@ -2,4 +2,5 @@ aiofiles
pydantic
opencv-python
imageio-ffmpeg
brotli
# logfire

View File

@ -2,6 +2,7 @@ import { app } from "./app.js";
import { api } from "./api.js";
import { ComfyWidgets, LGraphNode } from "./widgets.js";
import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25";
import { ComfyDeploy } from "https://esm.sh/comfydeploy@0.0.19-beta.30";
const 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>`;
@ -24,10 +25,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 +45,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 +144,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 +161,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 +184,7 @@ const ext = {
window.history.replaceState(
{},
document.title,
window.location.pathname
window.location.pathname,
);
});
}
@ -227,7 +214,7 @@ const ext = {
multiline: false,
},
],
app
app,
);
ComfyWidgets.STRING(
@ -240,17 +227,14 @@ const ext = {
multiline: false,
},
],
app
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 +261,7 @@ const ext = {
title_mode: LiteGraph.NORMAL_TITLE,
title: "Comfy Deploy",
collapsable: true,
})
}),
);
ComfyDeploy.category = "deploy";
@ -310,9 +294,7 @@ const ext = {
if (typeof api.handlePromptGenerated === "function") {
api.handlePromptGenerated(prompt);
} else {
console.warn(
"api.handlePromptGenerated is not a function"
);
console.warn("api.handlePromptGenerated is not a function");
}
sendEventToCD("cd_plugin_onQueuePrompt", prompt);
} else if (message.type === "get_prompt") {
@ -346,13 +328,9 @@ const ext = {
const canvas = app.canvas;
const targetScale = 1;
const targetOffsetX =
canvas.canvas.width / 4 -
position[0] -
node.size[0] / 2;
canvas.canvas.width / 4 - position[0] - node.size[0] / 2;
const targetOffsetY =
canvas.canvas.height / 4 -
position[1] -
node.size[1] / 2;
canvas.canvas.height / 4 - position[1] - node.size[1] / 2;
const startScale = canvas.ds.scale;
const startOffsetX = canvas.ds.offset[0];
@ -376,21 +354,9 @@ const ext = {
const easedT = easeOutCubic(t);
const currentScale = lerp(
startScale,
targetScale,
easedT
);
const currentOffsetX = lerp(
startOffsetX,
targetOffsetX,
easedT
);
const currentOffsetY = lerp(
startOffsetY,
targetOffsetY,
easedT
);
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];
@ -442,7 +408,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> `,
);
}
@ -550,7 +516,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 +557,7 @@ async function deployWorkflow() {
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label>
</div>
`
`,
);
if (!ok) return;
@ -610,7 +576,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 +597,7 @@ async function deployWorkflow() {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
}
},
)
.then((x) => x.json())
.catch(() => {
@ -650,7 +616,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,44 +638,36 @@ 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);
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;
}
},
// 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,
});
@ -734,12 +692,21 @@ async function deployWorkflow() {
"Check dependencies",
// JSON.stringify(deps, null, 2),
`
<div>
You will need to create a cloud machine with the following configuration on ComfyDeploy
<ol style="text-align: left; margin-top: 10px;">
<li>Review the dependencies listed in the graph below</li>
<li>Create a new cloud machine with the required configuration</li>
<li>Install missing models and check missing files</li>
<li>Deploy your workflow to the newly created machine</li>
</ol>
</div>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
<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 +767,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(() => {
@ -997,22 +964,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 +1035,7 @@ export class ConfirmDialog extends InfoDialog {
this.close();
},
}),
]
],
),
];
}
@ -1130,7 +1092,7 @@ function getData(environment) {
function saveData(data) {
localStorage.setItem(
"comfy_deploy_env_data_" + data.environment,
JSON.stringify(data)
JSON.stringify(data),
);
}
@ -1145,9 +1107,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 +1141,7 @@ export class ConfigDialog extends ComfyDialog {
this.close();
},
}),
]
],
),
];
}
@ -1193,8 +1153,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,12 +1185,8 @@ 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%;">
@ -1249,9 +1204,7 @@ export class ConfigDialog extends ComfyDialog {
}">
<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 +1225,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 +1236,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();
}
@ -1327,3 +1279,167 @@ export class ConfigDialog extends ComfyDialog {
}
export const configDialog = new ConfigDialog();
const currentOrigin = window.location.origin;
const client = new ComfyDeploy({
bearerAuth: getData().apiKey,
serverURL: `${currentOrigin}/comfydeploy/api/`,
});
app.extensionManager.registerSidebarTab({
id: "search",
icon: "pi pi-cloud-upload",
title: "Deploy",
tooltip: "Deploy and Configure",
type: "custom",
render: (el) => {
el.innerHTML = `
<div style="padding: 20px;">
<h3>Comfy Deploy</h3>
<div id="deploy-container" style="margin-bottom: 20px;"></div>
<div id="workflows-container">
<h4>Your Workflows</h4>
<ul id="workflows-list" style="list-style-type: none; padding: 0;"></ul>
</div>
<div id="config-container"></div>
</div>
`;
// Add deploy button
const deployContainer = el.querySelector("#deploy-container");
const deployButton = document.createElement("button");
deployButton.id = "sidebar-deploy-button";
deployButton.style.display = "flex";
deployButton.style.alignItems = "center";
deployButton.style.justifyContent = "center";
deployButton.style.width = "100%";
deployButton.style.marginBottom = "10px";
deployButton.style.padding = "10px";
deployButton.style.fontSize = "16px";
deployButton.style.fontWeight = "bold";
deployButton.style.backgroundColor = "#4CAF50";
deployButton.style.color = "white";
deployButton.style.border = "none";
deployButton.style.borderRadius = "5px";
deployButton.style.cursor = "pointer";
deployButton.innerHTML = `<i class="pi pi-cloud-upload" style="margin-right: 8px;"></i><div id='sidebar-button-title'>Deploy</div>`;
deployButton.onclick = async () => {
await deployWorkflow();
};
deployContainer.appendChild(deployButton);
// Add config button
const configContainer = el.querySelector("#config-container");
const configButton = document.createElement("button");
configButton.style.display = "flex";
configButton.style.alignItems = "center";
configButton.style.justifyContent = "center";
configButton.style.width = "100%";
configButton.style.padding = "8px";
configButton.style.fontSize = "14px";
configButton.style.backgroundColor = "#f0f0f0";
configButton.style.color = "#333";
configButton.style.border = "1px solid #ccc";
configButton.style.borderRadius = "5px";
configButton.style.cursor = "pointer";
configButton.innerHTML = `<i class="pi pi-cog" style="margin-right: 8px;"></i>Configure`;
configButton.onclick = () => {
configDialog.show();
};
deployContainer.appendChild(configButton);
// Fetch and display workflows
const workflowsList = el.querySelector("#workflows-list");
client.workflows
.getAll({
page: "1",
pageSize: "10",
})
.then((result) => {
result.forEach((workflow) => {
const li = document.createElement("li");
li.style.marginBottom = "15px";
li.style.padding = "15px";
li.style.backgroundColor = "#2a2a2a";
li.style.borderRadius = "8px";
li.style.boxShadow = "0 2px 4px rgba(0,0,0,0.1)";
const lastRun = workflow.runs[0];
const lastRunStatus = lastRun ? lastRun.status : "No runs";
const statusColor =
lastRunStatus === "success"
? "#4CAF50"
: lastRunStatus === "error"
? "#F44336"
: "#FFC107";
const timeAgo = getTimeAgo(new Date(workflow.updatedAt));
li.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<strong style="font-size: 18px; color: #e0e0e0;">${workflow.name}</strong>
</div>
<span style="font-size: 12px; color: ${statusColor}; margin-left: 10px;">Last run: ${lastRunStatus}</span>
</div>
<div style="font-size: 14px; color: #bdbdbd; margin-bottom: 10px;">Last updated ${timeAgo}</div>
<div style="display: flex; gap: 10px;">
<button class="open-cloud-btn" style="padding: 5px 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Open in Cloud</button>
<button class="load-api-btn" style="padding: 5px 10px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">Load Workflow</button>
</div>
`;
const openCloudBtn = li.querySelector(".open-cloud-btn");
openCloudBtn.onclick = () =>
window.open(
`${getData().endpoint}/workflows/${workflow.id}?workspace=true`,
"_blank",
);
const loadApiBtn = li.querySelector(".load-api-btn");
loadApiBtn.onclick = () => loadWorkflowApi(workflow.versions[0].id);
workflowsList.appendChild(li);
});
})
.catch((error) => {
console.error("Error fetching workflows:", error);
workflowsList.innerHTML =
"<li style='color: #F44336;'>Error fetching workflows</li>";
});
},
});
function getTimeAgo(date) {
const seconds = Math.floor((new Date() - date) / 1000);
let interval = seconds / 31536000;
if (interval > 1) return Math.floor(interval) + " years ago";
interval = seconds / 2592000;
if (interval > 1) return Math.floor(interval) + " months ago";
interval = seconds / 86400;
if (interval > 1) return Math.floor(interval) + " days ago";
interval = seconds / 3600;
if (interval > 1) return Math.floor(interval) + " hours ago";
interval = seconds / 60;
if (interval > 1) return Math.floor(interval) + " minutes ago";
return Math.floor(seconds) + " seconds ago";
}
async function loadWorkflowApi(versionId) {
try {
const response = await client.comfyui.getWorkflowVersionVersionId({
versionId: versionId,
});
// Implement the logic to load the workflow API into the ComfyUI interface
console.log("Workflow API loaded:", response);
await window["app"].ui.settings.setSettingValueAsync(
"Comfy.Validation.Workflows",
false,
);
app.loadGraphData(response.workflow);
// You might want to update the UI or trigger some action in ComfyUI here
} catch (error) {
console.error("Error loading workflow API:", error);
// Show an error message to the user
}
}