Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd150aa20f | ||
|
|
17653508c5 | ||
|
|
ace12efd1a | ||
|
|
01e8668d1a | ||
|
|
8e58d962a7 | ||
|
|
c59c308d32 | ||
|
|
4560f2cca9 | ||
|
|
872752b820 | ||
|
|
97bb2b69c5 | ||
|
|
08fe87c8af | ||
|
|
d43e5fcefc | ||
|
|
65492a108c |
@@ -93,6 +93,10 @@ Major areas
|
|||||||
|
|
||||||
# Self Hosting with Vercel
|
# Self Hosting with Vercel
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=hWvsEY1cS2M)
|
||||||
|
Tutorial Created by [Ross](https://github.com/rossman22590) and [Syn](https://github.com/mortlsyn)
|
||||||
|
|
||||||
|
|
||||||
Build command
|
Build command
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -361,13 +361,30 @@ async def send_json_override(self, event, data, sid=None):
|
|||||||
if event == 'executing' and data.get('node') is not None:
|
if event == 'executing' and data.get('node') is not None:
|
||||||
node = data.get('node')
|
node = data.get('node')
|
||||||
|
|
||||||
if 'prompt_id' in prompt_metadata:
|
if prompt_id in prompt_metadata:
|
||||||
|
if 'progress' not in prompt_metadata[prompt_id]:
|
||||||
|
prompt_metadata[prompt_id]["progress"] = set()
|
||||||
|
|
||||||
|
prompt_metadata[prompt_id]["progress"].add(node)
|
||||||
|
calculated_progress = len(prompt_metadata[prompt_id]["progress"]) / len(prompt_metadata[prompt_id]['workflow_api'])
|
||||||
|
# print("calculated_progress", calculated_progress)
|
||||||
|
|
||||||
if 'last_updated_node' in prompt_metadata[prompt_id] and prompt_metadata[prompt_id]['last_updated_node'] == node:
|
if 'last_updated_node' in prompt_metadata[prompt_id] and prompt_metadata[prompt_id]['last_updated_node'] == node:
|
||||||
return
|
return
|
||||||
prompt_metadata[prompt_id]['last_updated_node'] = node
|
prompt_metadata[prompt_id]['last_updated_node'] = node
|
||||||
class_type = prompt_metadata[prompt_id]['workflow_api'][node]['class_type']
|
class_type = prompt_metadata[prompt_id]['workflow_api'][node]['class_type']
|
||||||
print("updating run live status", class_type)
|
print("updating run live status", class_type)
|
||||||
await update_run_live_status(prompt_id, "Executing " + class_type)
|
await update_run_live_status(prompt_id, "Executing " + class_type, calculated_progress)
|
||||||
|
|
||||||
|
if event == 'execution_cached' and data.get('nodes') is not None:
|
||||||
|
if prompt_id in prompt_metadata:
|
||||||
|
if 'progress' not in prompt_metadata[prompt_id]:
|
||||||
|
prompt_metadata[prompt_id]["progress"] = set()
|
||||||
|
|
||||||
|
if 'nodes' in data:
|
||||||
|
for node in data.get('nodes', []):
|
||||||
|
prompt_metadata[prompt_id]["progress"].add(node)
|
||||||
|
# prompt_metadata[prompt_id]["progress"].update(data.get('nodes'))
|
||||||
|
|
||||||
if event == 'execution_error':
|
if event == 'execution_error':
|
||||||
# Careful this might not be fully awaited.
|
# Careful this might not be fully awaited.
|
||||||
@@ -391,18 +408,22 @@ class Status(Enum):
|
|||||||
# Global variable to keep track of the last read line number
|
# Global variable to keep track of the last read line number
|
||||||
last_read_line_number = 0
|
last_read_line_number = 0
|
||||||
|
|
||||||
async def update_run_live_status(prompt_id, live_status):
|
async def update_run_live_status(prompt_id, live_status, calculated_progress: float):
|
||||||
if prompt_id not in prompt_metadata:
|
if prompt_id not in prompt_metadata:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
print("progress", calculated_progress)
|
||||||
|
|
||||||
status_endpoint = prompt_metadata[prompt_id]['status_endpoint']
|
status_endpoint = prompt_metadata[prompt_id]['status_endpoint']
|
||||||
body = {
|
body = {
|
||||||
"run_id": prompt_id,
|
"run_id": prompt_id,
|
||||||
"live_status": live_status,
|
"live_status": live_status,
|
||||||
|
"progress": calculated_progress
|
||||||
}
|
}
|
||||||
# requests.post(status_endpoint, json=body)
|
# requests.post(status_endpoint, json=body)
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
await session.post(status_endpoint, json=body)
|
async with session.post(status_endpoint, json=body) as response:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def update_run(prompt_id, status: Status):
|
def update_run(prompt_id, status: Status):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { app } from "./app.js";
|
import { app } from "./app.js";
|
||||||
import { api } from "./api.js";
|
import { api } from "./api.js";
|
||||||
import { ComfyWidgets, LGraphNode } from "./widgets.js";
|
import { ComfyWidgets, LGraphNode } from "./widgets.js";
|
||||||
import { generateDependencyGraph } from "https://esm.sh/[email protected]4";
|
import { generateDependencyGraph } from "https://esm.sh/[email protected]9";
|
||||||
|
|
||||||
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
|
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
|
||||||
/** @type {ComfyExtension} */
|
/** @type {ComfyExtension} */
|
||||||
@@ -182,7 +182,7 @@ function showError(title, message) {
|
|||||||
function createDynamicUIHtml(data) {
|
function createDynamicUIHtml(data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
let html =
|
let html =
|
||||||
'<div style="max-width: 1024px; margin: 14px auto; display: flex; flex-direction: column; gap: 24px;">';
|
'<div style="min-width: 600px; max-width: 1024px; margin: 14px auto; display: flex; flex-direction: column; gap: 24px;">';
|
||||||
const bgcolor = "var(--comfy-input-bg)";
|
const bgcolor = "var(--comfy-input-bg)";
|
||||||
const evenBg = "var(--border-color)";
|
const evenBg = "var(--border-color)";
|
||||||
const textColor = "var(--input-text)";
|
const textColor = "var(--input-text)";
|
||||||
@@ -284,8 +284,12 @@ function addButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ok = await confirmDialog.confirm(
|
const ok = await confirmDialog.confirm(
|
||||||
"Confirm deployment -> " + displayName,
|
"Confirm deployment -> " +
|
||||||
`A new version will be deployed, are you conform? <br><br><input id="include-deps" type="checkbox" checked>Include dependence</input>`,
|
displayName +
|
||||||
|
"<br><br><small><div style='font-weight: normal;'>" +
|
||||||
|
endpoint +
|
||||||
|
"<div><br>",
|
||||||
|
`A new version will be deployed, are you confirm? <br><br><input id="include-deps" type="checkbox" checked>Include dependence</input>`,
|
||||||
);
|
);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
const pathName = usePathname();
|
||||||
|
return <LoadingPageWrapper className="h-full" tag={pathName.toLowerCase()} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
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',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Txt2Img SDXL Turbo",
|
||||||
|
description: "Try SDXL turbo and generate images since 1 step in seconds.",
|
||||||
|
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-txt2img-sdxl-turbo',
|
||||||
|
image: {
|
||||||
|
src: '/example-workflows/txt2img-sdxl-turbo.webp',
|
||||||
|
alt: 'Txt2Img SDXL Turbo',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Img2Img SDXL Controlnet",
|
||||||
|
description: "This workflow uses canny. Generate lines of you original image and create variations.",
|
||||||
|
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-img2-img-sdxl-controlnet',
|
||||||
|
image: {
|
||||||
|
src: '/example-workflows/txt2img-controlnet.webp',
|
||||||
|
alt: 'Img2Img SDXL Controlnet',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Automatic Inpainting (SEG)",
|
||||||
|
description: "Type what do you want to select and change that area with your prompt.",
|
||||||
|
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-automatic-inpainting-clip-seg',
|
||||||
|
image: {
|
||||||
|
src: '/example-workflows/automatic-inpainting-seg.webp',
|
||||||
|
alt: 'Img2Img SDXL Controlnet',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
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-5">
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useMediaQuery } from "usehooks-ts";
|
import { useMediaQuery } from "usehooks-ts";
|
||||||
|
|
||||||
@@ -25,8 +24,6 @@ export function NavbarMenu({
|
|||||||
const pathnames = usePathname();
|
const pathnames = usePathname();
|
||||||
const pathname = `/${pathnames.split("/")[1]}`;
|
const pathname = `/${pathnames.split("/")[1]}`;
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const pages = [
|
const pages = [
|
||||||
{
|
{
|
||||||
name: "Workflows",
|
name: "Workflows",
|
||||||
@@ -40,6 +37,10 @@ export function NavbarMenu({
|
|||||||
name: "API Keys",
|
name: "API Keys",
|
||||||
path: "/api-keys",
|
path: "/api-keys",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Examples",
|
||||||
|
path: "/examples"
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,18 +49,15 @@ export function NavbarMenu({
|
|||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultValue={pathname}
|
defaultValue={pathname}
|
||||||
className="w-[300px] flex pointer-events-auto"
|
className="w-fit flex pointer-events-auto"
|
||||||
>
|
>
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<TabsList className="w-full">
|
||||||
{pages.map((page) => (
|
{pages.map((page) => (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={page.name}
|
key={page.name}
|
||||||
value={page.path}
|
value={page.path}
|
||||||
onClick={() => {
|
|
||||||
router.push(page.path);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{page.name}
|
<Link href={page.path}>{page.name}</Link>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ export function RunWorkflowButton({
|
|||||||
className="px-1"
|
className="px-1"
|
||||||
>
|
>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<AutoFormSubmit>
|
<AutoFormSubmit disabled={isLoading}>
|
||||||
Run
|
Run
|
||||||
{isLoading ? <LoadingIcon /> : <Play size={14} />}
|
{isLoading ? <LoadingIcon /> : <Play size={14} />}
|
||||||
</AutoFormSubmit>
|
</AutoFormSubmit>
|
||||||
|
|||||||
@@ -90,12 +90,12 @@ export const createRun = withServerPromise(
|
|||||||
Object.entries(workflow_api).forEach(([_, node]) => {
|
Object.entries(workflow_api).forEach(([_, node]) => {
|
||||||
if (node.inputs["input_id"] === key) {
|
if (node.inputs["input_id"] === key) {
|
||||||
node.inputs["input_id"] = inputs[key];
|
node.inputs["input_id"] = inputs[key];
|
||||||
|
// Fix for external text default value
|
||||||
|
if (node.class_type == "ComfyUIDeployExternalText") {
|
||||||
|
node.inputs["default_value"] = inputs[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix for external text default value
|
|
||||||
if (node.class_type == "ComfyUIDeployExternalText") {
|
|
||||||
node.inputs["default_value"] = inputs[key];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||