2
.github/workflows/build-and-publish.yml
vendored
2
.github/workflows/build-and-publish.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
pnpm build:css
|
pnpm prebuild
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
|
|||||||
2
.github/workflows/build-and-test.yml
vendored
2
.github/workflows/build-and-test.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
pnpm build:css
|
pnpm prebuild
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
|
|||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -5,3 +5,7 @@
|
|||||||
[submodule "litegraph"]
|
[submodule "litegraph"]
|
||||||
path = litegraph
|
path = litegraph
|
||||||
url = https://github.com/space-nuko/litegraph.ts
|
url = https://github.com/space-nuko/litegraph.ts
|
||||||
|
[submodule "klecks"]
|
||||||
|
path = klecks
|
||||||
|
url = https://github.com/space-nuko/klecks
|
||||||
|
branch = comfybox
|
||||||
|
|||||||
1
klecks
Submodule
1
klecks
Submodule
Submodule klecks added at a36de3203f
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "web2",
|
"name": "ComfyBox",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
"format": "prettier --plugin-search-dir . --write .",
|
"format": "prettier --plugin-search-dir . --write .",
|
||||||
"svelte-check": "svelte-check",
|
"svelte-check": "svelte-check",
|
||||||
|
"prebuild": "pnpm run build:css && pnpm --filter=klecks lang:build",
|
||||||
"build:css": "pollen -c gradio/js/theme/src/pollen.config.cjs && mv src/pollen.css node_modules/@gradio/theme/src"
|
"build:css": "pollen -c gradio/js/theme/src/pollen.config.cjs && mv src/pollen.css node_modules/@gradio/theme/src"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"svelte-dnd-action": "^0.9.22",
|
"svelte-dnd-action": "^0.9.22",
|
||||||
"typescript": "^5.0.3",
|
"typescript": "^5.0.3",
|
||||||
"vite": "^4.3.1",
|
"vite": "^4.3.1",
|
||||||
|
"vite-plugin-glsl": "^1.1.2",
|
||||||
"vite-plugin-static-copy": "^0.14.0",
|
"vite-plugin-static-copy": "^0.14.0",
|
||||||
"vite-plugin-svelte-console-remover": "^1.0.10",
|
"vite-plugin-svelte-console-remover": "^1.0.10",
|
||||||
"vite-tsconfig-paths": "^4.0.8",
|
"vite-tsconfig-paths": "^4.0.8",
|
||||||
@@ -62,6 +64,7 @@
|
|||||||
"framework7": "^8.0.3",
|
"framework7": "^8.0.3",
|
||||||
"framework7-svelte": "^8.0.3",
|
"framework7-svelte": "^8.0.3",
|
||||||
"img-comparison-slider": "^8.0.0",
|
"img-comparison-slider": "^8.0.0",
|
||||||
|
"klecks": "workspace:*",
|
||||||
"pollen-css": "^4.6.2",
|
"pollen-css": "^4.6.2",
|
||||||
"radix-icons-svelte": "^1.2.1",
|
"radix-icons-svelte": "^1.2.1",
|
||||||
"svelte-feather-icons": "^4.0.0",
|
"svelte-feather-icons": "^4.0.0",
|
||||||
@@ -71,6 +74,7 @@
|
|||||||
"svelte-tiny-virtual-list": "^2.0.5",
|
"svelte-tiny-virtual-list": "^2.0.5",
|
||||||
"tailwindcss": "^3.3.1",
|
"tailwindcss": "^3.3.1",
|
||||||
"typed-emitter": "github:andywer/typed-emitter",
|
"typed-emitter": "github:andywer/typed-emitter",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"vite-plugin-full-reload": "^1.0.5"
|
"vite-plugin-full-reload": "^1.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1670
pnpm-lock.yaml
generated
1670
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,3 +2,4 @@ packages:
|
|||||||
- 'gradio/js/*'
|
- 'gradio/js/*'
|
||||||
- 'gradio/client/js'
|
- 'gradio/client/js'
|
||||||
- 'litegraph/packages/*'
|
- 'litegraph/packages/*'
|
||||||
|
- 'klecks'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem, Subgraph } from "@litegraph-ts/core";
|
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem, Subgraph, LLink } from "@litegraph-ts/core";
|
||||||
import type ComfyApp from "./components/ComfyApp";
|
import type ComfyApp from "./components/ComfyApp";
|
||||||
import queueState from "./stores/queueState";
|
import queueState from "./stores/queueState";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
@@ -259,66 +259,59 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
|
|
||||||
const newNode = LiteGraph.createNode(node.type);
|
const newNode = LiteGraph.createNode(node.type);
|
||||||
|
|
||||||
for (let index = 0; index < newNode.inputs.length; index++) {
|
const createInputReroute = (slotIndex: number, link: LLink | null): ComfyReroute => {
|
||||||
const newInput = newNode.inputs[index];
|
|
||||||
const oldInput = node.inputs[index]
|
|
||||||
|
|
||||||
if (oldInput && newInput.type === oldInput.type) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let link: LLink | null = null;
|
|
||||||
|
|
||||||
if (oldInput) {
|
|
||||||
link = node.getInputLink(index);
|
|
||||||
node.disconnectInput(index)
|
|
||||||
oldInput.type = newInput.type
|
|
||||||
oldInput.name = newInput.name
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
node.addInput(newInput.name, newInput.type, newInput)
|
|
||||||
}
|
|
||||||
|
|
||||||
const reroute = LiteGraph.createNode(ComfyReroute);
|
const reroute = LiteGraph.createNode(ComfyReroute);
|
||||||
reroute.properties.ignoreTypes = true;
|
reroute.properties.ignoreTypes = true;
|
||||||
node.graph.add(reroute)
|
node.graph.add(reroute)
|
||||||
const inputPos = node.getConnectionPos(true, index);
|
const inputPos = node.getConnectionPos(true, slotIndex);
|
||||||
reroute.pos = [inputPos[0] - 140, inputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2];
|
reroute.pos = [inputPos[0] - 140, inputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2];
|
||||||
reroute.connect(0, node, index);
|
node.graph.getNodeById(link.origin_id).connect(link.origin_slot, reroute, 0)
|
||||||
if (link != null)
|
return reroute
|
||||||
node.graph.getNodeById(link.target_id).connect(link.target_slot, reroute, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let index = 0; index < newNode.outputs.length; index++) {
|
for (let index = node.inputs.length - 1; index >= 0; index--) {
|
||||||
const newOutput = newNode.outputs[index];
|
let link: LLink | null = null;
|
||||||
const oldOutput = node.outputs[index]
|
|
||||||
|
|
||||||
if (oldOutput && newOutput.type === oldOutput.type) {
|
link = node.getInputLink(index);
|
||||||
continue;
|
node.disconnectInput(index)
|
||||||
}
|
|
||||||
|
|
||||||
let links = []
|
if (link)
|
||||||
|
createInputReroute(index, link);
|
||||||
|
|
||||||
if (oldOutput) {
|
node.removeInput(index);
|
||||||
links = node.getOutputLinks(index)
|
}
|
||||||
node.disconnectOutput(index)
|
|
||||||
oldOutput.type = newOutput.type
|
|
||||||
oldOutput.name = newOutput.name
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
node.addOutput(newOutput.name, newOutput.type, newOutput)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (let index = 0; index < newNode.inputs.length; index++) {
|
||||||
|
const newInput = newNode.inputs[index]
|
||||||
|
const input = node.addInput(newInput.name, newInput.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createOutputReroute = (index: number, links: LLink[], connect: boolean = true): ComfyReroute => {
|
||||||
const reroute = LiteGraph.createNode(ComfyReroute);
|
const reroute = LiteGraph.createNode(ComfyReroute);
|
||||||
reroute.properties.ignoreTypes = true;
|
reroute.properties.ignoreTypes = true;
|
||||||
node.graph.add(reroute)
|
node.graph.add(reroute)
|
||||||
const rerouteSize = reroute.computeSize();
|
const rerouteSize = reroute.computeSize();
|
||||||
const outputPos = node.getConnectionPos(false, index);
|
const outputPos = node.getConnectionPos(false, index);
|
||||||
reroute.pos = [outputPos[0] + rerouteSize[0] + 20, outputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2];
|
reroute.pos = [outputPos[0] + rerouteSize[0] + 20, outputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2];
|
||||||
node.connect(index, reroute, 0);
|
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
reroute.connect(0, link.target_id, link.target_slot)
|
reroute.connect(0, node.graph.getNodeById(link.target_id), link.target_slot)
|
||||||
}
|
}
|
||||||
|
return reroute
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = node.outputs.length - 1; index >= 0; index--) {
|
||||||
|
let links = node.getOutputLinks(index)
|
||||||
|
node.disconnectOutput(index)
|
||||||
|
|
||||||
|
if (links.length > 0)
|
||||||
|
createOutputReroute(index, links);
|
||||||
|
|
||||||
|
node.removeOutput(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < newNode.outputs.length; index++) {
|
||||||
|
const newOutput = newNode.outputs[index]
|
||||||
|
const output = node.addOutput(newOutput.name, newOutput.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutput, SerializedPromptOutputs } from "./components/ComfyApp";
|
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutput, SerializedPromptOutputs } from "./components/ComfyApp";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type { GalleryOutput, GalleryOutputEntry } from "./nodes/ComfyWidgetNodes";
|
import type { ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes";
|
||||||
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
|
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
|
||||||
import type { SerializedLayoutState } from "./stores/layoutState";
|
import type { SerializedLayoutState } from "./stores/layoutState";
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export type ComfyPromptPNGInfo = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ComfyBoxPromptExtraData = ComfyUIPromptExtraData & {
|
export type ComfyBoxPromptExtraData = ComfyUIPromptExtraData & {
|
||||||
thumbnails?: GalleryOutputEntry[],
|
thumbnails?: ComfyImageLocation[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComfyUIPromptExtraData = {
|
export type ComfyUIPromptExtraData = {
|
||||||
@@ -258,9 +258,13 @@ export default class ComfyAPI {
|
|||||||
},
|
},
|
||||||
body: postBody
|
body: postBody
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(async (res) => {
|
||||||
|
if (res.status != 200) {
|
||||||
|
throw await res.text()
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
})
|
||||||
.then(raw => { return { promptID: raw.prompt_id } })
|
.then(raw => { return { promptID: raw.prompt_id } })
|
||||||
.catch(res => { throw res.text() })
|
|
||||||
.catch(error => { return { error } })
|
.catch(error => { return { error } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { download, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis }
|
|||||||
import notify from "$lib/notify";
|
import notify from "$lib/notify";
|
||||||
import configState from "$lib/stores/configState";
|
import configState from "$lib/stores/configState";
|
||||||
import { blankGraph } from "$lib/defaultGraph";
|
import { blankGraph } from "$lib/defaultGraph";
|
||||||
import type { GalleryOutput } from "$lib/nodes/ComfyWidgetNodes";
|
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
|
|
||||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export type SerializedPrompt = {
|
|||||||
output: SerializedPromptInputsAll
|
output: SerializedPromptInputsAll
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedPromptOutputs = Record<NodeID, GalleryOutput>
|
export type SerializedPromptOutputs = Record<NodeID, ComfyExecutionResult>
|
||||||
|
|
||||||
export type Progress = {
|
export type Progress = {
|
||||||
value: number,
|
value: number,
|
||||||
@@ -245,7 +245,7 @@ export default class ComfyApp {
|
|||||||
|
|
||||||
const node: LGraphNodeConstructor = {
|
const node: LGraphNodeConstructor = {
|
||||||
class: ctor,
|
class: ctor,
|
||||||
title: nodeData.name,
|
title: nodeData.display_name || nodeData.name,
|
||||||
type: nodeId,
|
type: nodeId,
|
||||||
desc: `ComfyNode: ${nodeId}`
|
desc: `ComfyNode: ${nodeId}`
|
||||||
}
|
}
|
||||||
@@ -347,7 +347,7 @@ export default class ComfyApp {
|
|||||||
this.lGraph.setDirtyCanvas(true, false);
|
this.lGraph.setDirtyCanvas(true, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.api.addEventListener("executed", (promptID: PromptID, nodeID: NodeID, output: GalleryOutput) => {
|
this.api.addEventListener("executed", (promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) => {
|
||||||
this.nodeOutputs[nodeID] = output;
|
this.nodeOutputs[nodeID] = output;
|
||||||
const node = this.lGraph.getNodeById(nodeID) as ComfyGraphNode;
|
const node = this.lGraph.getNodeById(nodeID) as ComfyGraphNode;
|
||||||
if (node?.onExecuted) {
|
if (node?.onExecuted) {
|
||||||
@@ -408,7 +408,8 @@ export default class ComfyApp {
|
|||||||
setColor(type, "orange")
|
setColor(type, "orange")
|
||||||
}
|
}
|
||||||
|
|
||||||
setColor("IMAGE", "rebeccapurple")
|
setColor("COMFYBOX_IMAGES", "rebeccapurple")
|
||||||
|
setColor("COMFYBOX_IMAGE", "fuchsia")
|
||||||
setColor(BuiltInSlotType.EVENT, "lightseagreen")
|
setColor(BuiltInSlotType.EVENT, "lightseagreen")
|
||||||
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
||||||
}
|
}
|
||||||
@@ -785,15 +786,15 @@ export default class ComfyApp {
|
|||||||
queueState.afterQueued(promptID, num, p.output, extraData)
|
queueState.afterQueued(promptID, num, p.output, extraData)
|
||||||
|
|
||||||
error = response.error;
|
error = response.error;
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
error = error.toString();
|
error = err
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
const mes = error.response || error.toString()
|
const mes = error.response || error.toString()
|
||||||
notify(`Error queuing prompt:\n${mes}`, { type: "error" })
|
notify(`Error queuing prompt:\n${mes}`, { type: "error" })
|
||||||
console.error(promptToGraphVis(p))
|
console.error(promptToGraphVis(p))
|
||||||
console.error("Error queuing prompt", mes, num, p)
|
console.error("Error queuing prompt", error, num, p)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -339,6 +339,7 @@
|
|||||||
border-bottom: 1px solid var(--block-border-color);
|
border-bottom: 1px solid var(--block-border-color);
|
||||||
border-top: 1px solid var(--table-border-color);
|
border-top: 1px solid var(--table-border-color);
|
||||||
background: var(--panel-background-fill);
|
background: var(--panel-background-fill);
|
||||||
|
max-height: 14rem;
|
||||||
|
|
||||||
&:hover:not(:has(img:hover)) {
|
&:hover:not(:has(img:hover)) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -387,6 +388,7 @@
|
|||||||
column-gap: 1px;
|
column-gap: 1px;
|
||||||
row-gap: 1px;
|
row-gap: 1px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
flex: 1 1 40%;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
|
|||||||
254
src/lib/components/ImageUpload.svelte
Normal file
254
src/lib/components/ImageUpload.svelte
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import UploadText from "$lib/components/gradio/app/UploadText.svelte";
|
||||||
|
import type { ComfyImageLocation } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
|
import notify from "$lib/notify";
|
||||||
|
import { convertComfyOutputEntryToGradio, convertComfyOutputToComfyURL, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
||||||
|
import { Block, BlockLabel } from "@gradio/atoms";
|
||||||
|
import { File as FileIcon } from "@gradio/icons";
|
||||||
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
|
import { ModifyUpload, Upload } from "@gradio/upload";
|
||||||
|
import { createEventDispatcher, tick } from "svelte";
|
||||||
|
|
||||||
|
export let value: ComfyImageLocation[] | null = null;
|
||||||
|
export let imgWidth: number = 0;
|
||||||
|
export let imgHeight: number = 0;
|
||||||
|
export let imgElem: HTMLImageElement | null = null
|
||||||
|
export let fileCount: "single" | "multiple" = "single"
|
||||||
|
export let elem_classes: string[] = []
|
||||||
|
export let style: string = ""
|
||||||
|
export let label: string = ""
|
||||||
|
// let propsChanged: Writable<number> | null = null;
|
||||||
|
let dragging = false;
|
||||||
|
let pending_upload = false;
|
||||||
|
let old_value: GradioFileData[] | null = null;
|
||||||
|
|
||||||
|
let _value: GradioFileData[] | null = null;
|
||||||
|
const root = "comf"
|
||||||
|
const root_url = "https//ComfyUI!"
|
||||||
|
let uploaded: boolean = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
change: ComfyImageLocation[];
|
||||||
|
uploading: undefined;
|
||||||
|
uploaded: ComfyImageLocation[];
|
||||||
|
upload_error: any;
|
||||||
|
clear: undefined;
|
||||||
|
image_clicked: undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
_value = null
|
||||||
|
if (imgElem)
|
||||||
|
imgElem.src = convertComfyOutputToComfyURL(value[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (!(_value && _value.length > 0 && imgElem)) {
|
||||||
|
imgWidth = 1
|
||||||
|
imgHeight = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange() {
|
||||||
|
dispatch("change", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpload() {
|
||||||
|
dispatch("uploaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
dispatch("clear")
|
||||||
|
}
|
||||||
|
|
||||||
|
function onImgClicked() {
|
||||||
|
dispatch("image_clicked")
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GradioUploadResponse {
|
||||||
|
error?: string;
|
||||||
|
files?: Array<ComfyImageLocation>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
|
||||||
|
console.debug("UPLOADILFES", root, files);
|
||||||
|
|
||||||
|
dispatch("uploading")
|
||||||
|
|
||||||
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
|
||||||
|
const requests = files.map(async (file) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("image", file, file.name);
|
||||||
|
return fetch(new Request(url + "/upload/image", {
|
||||||
|
body: formData,
|
||||||
|
method: 'POST'
|
||||||
|
}))
|
||||||
|
.then(r => r.json())
|
||||||
|
.catch(error => error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(requests)
|
||||||
|
.then( (results) => {
|
||||||
|
const errors = []
|
||||||
|
const files = []
|
||||||
|
|
||||||
|
for (const r of results) {
|
||||||
|
if (r instanceof Error) {
|
||||||
|
errors.push(r.cause)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// bare filename of image
|
||||||
|
const resp = r as ComfyUploadImageAPIResponse;
|
||||||
|
files.push({
|
||||||
|
filename: resp.name,
|
||||||
|
subfolder: "",
|
||||||
|
type: "input"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = null;
|
||||||
|
if (errors && errors.length > 0)
|
||||||
|
error = "Upload error(s):\n" + errors.join("\n");
|
||||||
|
|
||||||
|
return { error, files }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (JSON.stringify(_value) !== JSON.stringify(old_value) || uploaded) {
|
||||||
|
uploaded = false;
|
||||||
|
pending_upload = true;
|
||||||
|
|
||||||
|
old_value = _value;
|
||||||
|
|
||||||
|
if (_value == null)
|
||||||
|
_value = []
|
||||||
|
else if (!Array.isArray(_value))
|
||||||
|
_value = [_value]
|
||||||
|
|
||||||
|
const allBlobs = _value.map((file_data: GradioFileData) => file_data.blob)
|
||||||
|
|
||||||
|
if (allBlobs == null || allBlobs.length === 0) {
|
||||||
|
_value = null;
|
||||||
|
value = null;
|
||||||
|
onChange();
|
||||||
|
pending_upload = false;
|
||||||
|
}
|
||||||
|
else if (!allBlobs.every(b => b != null)) {
|
||||||
|
_value = null;
|
||||||
|
value = null;
|
||||||
|
onChange();
|
||||||
|
pending_upload = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let files = (Array.isArray(_value) ? _value : [_value]).map(
|
||||||
|
(file_data) => file_data.blob!
|
||||||
|
);
|
||||||
|
let upload_value = _value;
|
||||||
|
pending_upload = true;
|
||||||
|
upload_files(root, files).then((response) => {
|
||||||
|
if (JSON.stringify(upload_value) !== JSON.stringify(_value)) {
|
||||||
|
// value has changed since upload started
|
||||||
|
console.error("[ImageUpload] value has changed since upload started", upload_value, _value)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_upload = false;
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
notify(response.error, { type: "error" })
|
||||||
|
}
|
||||||
|
|
||||||
|
value = response.files;
|
||||||
|
dispatch("change", value)
|
||||||
|
dispatch("uploaded", value)
|
||||||
|
}).
|
||||||
|
catch(err => {
|
||||||
|
dispatch("upload_error", err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handle_upload({ detail }: CustomEvent<GradioFileData | Array<GradioFileData>>) {
|
||||||
|
// Received Gradio-format file data from the Upload component.
|
||||||
|
// In the reactive block above it will be uploaded to ComfyUI.
|
||||||
|
_value = Array.isArray(detail) ? detail : [detail];
|
||||||
|
uploaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_clear(_e: CustomEvent<null>) {
|
||||||
|
_value = null;
|
||||||
|
value = [];
|
||||||
|
dispatch("change", value)
|
||||||
|
dispatch("clear")
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertGradioUpload(e: CustomEvent<GradioFileData[]>) {
|
||||||
|
_value = e.detail
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="image-upload" {style}>
|
||||||
|
{#if value}
|
||||||
|
<Block
|
||||||
|
visible={true}
|
||||||
|
variant={(value === null || value.length === 0) ? "dashed" : "solid"}
|
||||||
|
border_mode={dragging ? "focus" : "base"}
|
||||||
|
padding={true}
|
||||||
|
elem_id="comfy-image-upload-block"
|
||||||
|
{elem_classes}
|
||||||
|
>
|
||||||
|
<BlockLabel
|
||||||
|
label={label}
|
||||||
|
show_label={label != ""}
|
||||||
|
Icon={FileIcon}
|
||||||
|
float={label != ""}
|
||||||
|
/>
|
||||||
|
{#if value && value.length > 0 && !pending_upload}
|
||||||
|
{@const firstImage = value[0]}
|
||||||
|
<ModifyUpload on:clear={handle_clear} absolute />
|
||||||
|
<img src={convertComfyOutputToComfyURL(firstImage)}
|
||||||
|
alt={firstImage.filename}
|
||||||
|
on:click={onImgClicked}
|
||||||
|
bind:this={imgElem}
|
||||||
|
bind:naturalWidth={imgWidth}
|
||||||
|
bind:naturalHeight={imgHeight}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Upload
|
||||||
|
file_count={fileCount}
|
||||||
|
filetype="image/*"
|
||||||
|
on:change={convertGradioUpload}
|
||||||
|
on:load={handle_upload}
|
||||||
|
bind:dragging
|
||||||
|
on:clear
|
||||||
|
on:select
|
||||||
|
parse_to_data_url={false}
|
||||||
|
>
|
||||||
|
<UploadText type="file" />
|
||||||
|
</Upload>
|
||||||
|
{/if}
|
||||||
|
</Block>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-upload {
|
||||||
|
height: var(--size-96);
|
||||||
|
|
||||||
|
:global(.block) {
|
||||||
|
height: inherit;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,23 +1,52 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
|
import { Button } from "@gradio/button";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
export let showModal; // boolean
|
export let showModal; // boolean
|
||||||
|
export let closeOnClick = true; // boolean
|
||||||
|
export const closeDialog = _ => doClose();
|
||||||
|
|
||||||
let dialog; // HTMLDialogElement
|
let dialog; // HTMLDialogElement
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
close: undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
$: if (dialog && showModal) dialog.showModal();
|
$: if (dialog && showModal) dialog.showModal();
|
||||||
|
|
||||||
|
function close(e: Event) {
|
||||||
|
if (!closeOnClick) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
doClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
function doClose() {
|
||||||
|
dialog.close();
|
||||||
|
dispatch("close")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<dialog
|
<dialog
|
||||||
bind:this={dialog}
|
bind:this={dialog}
|
||||||
on:close={() => (showModal = false)}
|
on:close={close}
|
||||||
on:click|self={() => dialog.close()}
|
on:cancel={doClose}
|
||||||
|
on:click|self={close}
|
||||||
>
|
>
|
||||||
<div on:click|stopPropagation>
|
<div on:click|stopPropagation>
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
<slot />
|
<slot />
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
<div class="button-row">
|
||||||
<button autofocus on:click={() => dialog.close()}>Close</button>
|
<slot name="buttons">
|
||||||
</div>
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
|
<Button variant="secondary" on:click={doClose}>Close</Button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -26,6 +55,7 @@
|
|||||||
border-radius: 0.2em;
|
border-radius: 0.2em;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
dialog::backdrop {
|
dialog::backdrop {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
@@ -58,4 +88,10 @@
|
|||||||
button {
|
button {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
110
src/lib/components/NumberInput.svelte
Normal file
110
src/lib/components/NumberInput.svelte
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { BlockTitle } from "@gradio/atoms";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
export let value: number = 0;
|
||||||
|
export let min: number = -1024
|
||||||
|
export let max: number = 1024
|
||||||
|
export let step: number = 1;
|
||||||
|
export let label: string = "";
|
||||||
|
export let disabled: boolean = false;
|
||||||
|
let inputValue = value;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{ change: number; release: number }>();
|
||||||
|
|
||||||
|
function handle_input(e: Event) {
|
||||||
|
const element = e.currentTarget as HTMLInputElement;
|
||||||
|
let newValue = parseFloat(element.value);
|
||||||
|
if (isNaN(newValue)) {
|
||||||
|
newValue = min;
|
||||||
|
}
|
||||||
|
inputValue = Math.min(Math.max(inputValue, min), max);
|
||||||
|
value = inputValue;
|
||||||
|
dispatch("release", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_release(e: MouseEvent) {
|
||||||
|
dispatch("release", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
inputValue = value;
|
||||||
|
dispatch("change", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamp = () => {
|
||||||
|
value = Math.min(Math.max(value, min), max);
|
||||||
|
dispatch("release", value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="head">
|
||||||
|
<label>
|
||||||
|
<BlockTitle>{label}</BlockTitle>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
data-testid="number-input"
|
||||||
|
type="number"
|
||||||
|
bind:value={inputValue}
|
||||||
|
on:input={handle_input}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
on:blur={clamp}
|
||||||
|
{step}
|
||||||
|
{disabled}
|
||||||
|
on:pointerup={handle_release}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> label {
|
||||||
|
padding: 0.5rem 1.0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type="number"] {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: var(--input-shadow);
|
||||||
|
border: var(--input-border-width) solid var(--input-border-color);
|
||||||
|
border-radius: var(--input-radius);
|
||||||
|
background: var(--input-background-fill);
|
||||||
|
padding: var(--size-2) var(--size-2);
|
||||||
|
color: var(--body-text-color);
|
||||||
|
font-size: var(--input-text-size);
|
||||||
|
line-height: var(--line-sm);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
input:disabled {
|
||||||
|
-webkit-text-fill-color: var(--body-text-color);
|
||||||
|
-webkit-opacity: 1;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]:focus {
|
||||||
|
box-shadow: var(--input-shadow-focus);
|
||||||
|
border-color: var(--input-border-color-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: var(--input-placeholder-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
58
src/lib/components/gradio/app/Column.svelte
Normal file
58
src/lib/components/gradio/app/Column.svelte
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { create_classes } from "@gradio/utils";
|
||||||
|
import type { Styles } from "@gradio/utils";
|
||||||
|
|
||||||
|
export let scale: number = 1;
|
||||||
|
export let min_width: number = 0;
|
||||||
|
export let elem_id: string = "";
|
||||||
|
export let elem_classes: Array<string> = [];
|
||||||
|
export let visible: boolean = true;
|
||||||
|
export let variant: "default" | "panel" | "compact" = "default";
|
||||||
|
export let style: Styles = {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id={elem_id}
|
||||||
|
class={elem_classes.join(" ")}
|
||||||
|
class:gap={style.gap !== false}
|
||||||
|
class:compact={variant === "compact"}
|
||||||
|
class:panel={variant === "panel"}
|
||||||
|
class:hide={!visible}
|
||||||
|
style={`min-width: min(${min_width}px, 100%); flex-grow: ${scale}`}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
div > :global(*),
|
||||||
|
div > :global(.form > *) {
|
||||||
|
width: var(--size-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap {
|
||||||
|
gap: var(--layout-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact > :global(*),
|
||||||
|
.compact :global(.box) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact,
|
||||||
|
.panel {
|
||||||
|
border: solid var(--panel-border-width) var(--panel-border-color);
|
||||||
|
border-radius: var(--container-radius);
|
||||||
|
background: var(--panel-background-fill);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
60
src/lib/components/gradio/app/Row.svelte
Normal file
60
src/lib/components/gradio/app/Row.svelte
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Styles } from "@gradio/utils";
|
||||||
|
|
||||||
|
export let style: Styles = {};
|
||||||
|
export let elem_id: string;
|
||||||
|
export let elem_classes: Array<string> = [];
|
||||||
|
export let visible: boolean = true;
|
||||||
|
export let variant: "default" | "panel" | "compact" = "default";
|
||||||
|
export let min_width: string = "160px";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class:compact={variant === "compact"}
|
||||||
|
class:panel={variant === "panel"}
|
||||||
|
class:unequal-height={style.equal_height === false}
|
||||||
|
class:stretch={style.equal_height}
|
||||||
|
class:hide={!visible}
|
||||||
|
id={elem_id}
|
||||||
|
class={elem_classes.join(" ")}
|
||||||
|
style="--row-min-width: {min_width}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--layout-gap);
|
||||||
|
width: var(--size-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.compact > :global(*),
|
||||||
|
.compact :global(.box) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.compact,
|
||||||
|
.panel {
|
||||||
|
border-radius: var(--container-radius);
|
||||||
|
background: var(--background-fill-secondary);
|
||||||
|
padding: var(--size-2);
|
||||||
|
}
|
||||||
|
.unequal-height {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stretch {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
div > :global(*),
|
||||||
|
div > :global(.form > *) {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
min-width: min(--row-min-width, 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,10 +5,10 @@ import queueState from "$lib/stores/queueState";
|
|||||||
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
|
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
||||||
import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
|
import type { ComfyWidgetNode, ComfyExecutionResult, ComfyImageLocation } from "./ComfyWidgetNodes";
|
||||||
import type { NotifyOptions } from "$lib/notify";
|
import type { NotifyOptions } from "$lib/notify";
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
import { convertComfyOutputToGradio, uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
import { convertComfyOutputToGradio, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
||||||
|
|
||||||
export class ComfyQueueEvents extends ComfyGraphNode {
|
export class ComfyQueueEvents extends ComfyGraphNode {
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
@@ -63,7 +63,7 @@ LiteGraph.registerNodeType({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties {
|
export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties {
|
||||||
images: GalleryOutput | null
|
images: ComfyExecutionResult | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComfyStoreImagesAction extends ComfyGraphNode {
|
export class ComfyStoreImagesAction extends ComfyGraphNode {
|
||||||
@@ -90,7 +90,7 @@ export class ComfyStoreImagesAction extends ComfyGraphNode {
|
|||||||
if (action !== "store" || !param || !("images" in param))
|
if (action !== "store" || !param || !("images" in param))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.setProperty("images", param as GalleryOutput)
|
this.setProperty("images", param as ComfyExecutionResult)
|
||||||
this.setOutputData(0, this.properties.images)
|
this.setOutputData(0, this.properties.images)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,7 @@ export class ComfyNotifyAction extends ComfyGraphNode {
|
|||||||
// native notifications.
|
// native notifications.
|
||||||
if (param != null && typeof param === "object") {
|
if (param != null && typeof param === "object") {
|
||||||
if ("images" in param) {
|
if ("images" in param) {
|
||||||
const output = param as GalleryOutput;
|
const output = param as ComfyExecutionResult;
|
||||||
const converted = convertComfyOutputToGradio(output);
|
const converted = convertComfyOutputToGradio(output);
|
||||||
if (converted.length > 0)
|
if (converted.length > 0)
|
||||||
options.imageUrl = converted[0].data;
|
options.imageUrl = converted[0].data;
|
||||||
@@ -581,86 +581,6 @@ LiteGraph.registerNodeType({
|
|||||||
type: "events/no_change"
|
type: "events/no_change"
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface ComfyUploadImageActionProperties extends ComfyGraphNodeProperties {
|
|
||||||
folderType: "output" | "temp"
|
|
||||||
lastUploadedImageFile: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyUploadImageAction extends ComfyGraphNode {
|
|
||||||
override properties: ComfyUploadImageActionProperties = {
|
|
||||||
tags: [],
|
|
||||||
folderType: "output",
|
|
||||||
lastUploadedImageFile: null
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "filename", type: "string" },
|
|
||||||
{ name: "trigger", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "input_filename", type: "string" },
|
|
||||||
{ name: "uploaded", type: BuiltInSlotType.EVENT }
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
private _promise = null;
|
|
||||||
|
|
||||||
displayWidget: ITextWidget;
|
|
||||||
|
|
||||||
constructor(title?: string) {
|
|
||||||
super(title);
|
|
||||||
this.displayWidget = this.addWidget<ITextWidget>(
|
|
||||||
"text",
|
|
||||||
"File",
|
|
||||||
this.properties.lastUploadedImageFile,
|
|
||||||
"lastUploadedImageFile"
|
|
||||||
);
|
|
||||||
this.displayWidget.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
override onExecute() {
|
|
||||||
this.setOutputData(0, this.properties.lastUploadedImageFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
override onAction(action: any, param: any) {
|
|
||||||
if (action !== "trigger" || this._promise != null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const filename = this.getInputData(0)
|
|
||||||
if (typeof filename !== "string" || !filename) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: GalleryOutputEntry = {
|
|
||||||
filename,
|
|
||||||
subfolder: "",
|
|
||||||
type: this.properties.folderType || "output"
|
|
||||||
}
|
|
||||||
|
|
||||||
this._promise = uploadImageToComfyUI(data)
|
|
||||||
.then((json: ComfyUploadImageAPIResponse) => {
|
|
||||||
console.debug("[UploadImageAction] Succeeded", json)
|
|
||||||
this.properties.lastUploadedImageFile = json.name;
|
|
||||||
this.triggerSlot(1, this.properties.lastUploadedImageFile);
|
|
||||||
this._promise = null;
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Error uploading:", e)
|
|
||||||
notify(`Error uploading image to ComfyUi: ${e}`, { type: "error", timeout: 10000 })
|
|
||||||
this.properties.lastUploadedImageFile = null;
|
|
||||||
this._promise = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyUploadImageAction,
|
|
||||||
title: "Comfy.UploadImageAction",
|
|
||||||
desc: "Uploads an image from the specified ComfyUI folder into its input folder",
|
|
||||||
type: "actions/store_images"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfySetPromptThumbnailsActionProperties extends ComfyGraphNodeProperties {
|
export interface ComfySetPromptThumbnailsActionProperties extends ComfyGraphNodeProperties {
|
||||||
defaultFolderType: string | null
|
defaultFolderType: string | null
|
||||||
}
|
}
|
||||||
@@ -679,12 +599,12 @@ export class ComfySetPromptThumbnailsAction extends ComfyGraphNode {
|
|||||||
|
|
||||||
_value: any = null;
|
_value: any = null;
|
||||||
|
|
||||||
override getPromptThumbnails(): GalleryOutputEntry[] | null {
|
override getPromptThumbnails(): ComfyImageLocation[] | null {
|
||||||
const data = this.getInputData(0)
|
const data = this.getInputData(0)
|
||||||
|
|
||||||
const folderType = this.properties.folderType || "input";
|
const folderType = this.properties.folderType || "input";
|
||||||
|
|
||||||
const convertString = (s: string): GalleryOutputEntry => {
|
const convertString = (s: string): ComfyImageLocation => {
|
||||||
return { filename: data, subfolder: "", type: folderType }
|
return { filename: data, subfolder: "", type: folderType }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,13 +613,13 @@ export class ComfySetPromptThumbnailsAction extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
else if (data != null && typeof data === "object") {
|
else if (data != null && typeof data === "object") {
|
||||||
if ("filename" in data && "type" in data)
|
if ("filename" in data && "type" in data)
|
||||||
return [data as GalleryOutputEntry];
|
return [data as ComfyImageLocation];
|
||||||
}
|
}
|
||||||
else if (Array.isArray(data) && data.length > 0) {
|
else if (Array.isArray(data) && data.length > 0) {
|
||||||
if (typeof data[0] === "string")
|
if (typeof data[0] === "string")
|
||||||
return data.map(convertString)
|
return data.map(convertString)
|
||||||
else if (typeof data[0] === "object" && "filename" in data[0] && "type" in data[0])
|
else if (typeof data[0] === "object" && "filename" in data[0] && "type" in data[0])
|
||||||
return data as GalleryOutputEntry[]
|
return data as ComfyImageLocation[]
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
||||||
import ComfyGraphNode from "./ComfyGraphNode";
|
import ComfyGraphNode from "./ComfyGraphNode";
|
||||||
import ComfyWidgets from "$lib/widgets"
|
import ComfyWidgets from "$lib/widgets"
|
||||||
import type { ComfyWidgetNode, GalleryOutput } from "./ComfyWidgetNodes";
|
import type { ComfyWidgetNode, ComfyExecutionResult } from "./ComfyWidgetNodes";
|
||||||
import { BuiltInSlotType, type SerializedLGraphNode } from "@litegraph-ts/core";
|
import { BuiltInSlotShape, BuiltInSlotType, type SerializedLGraphNode } from "@litegraph-ts/core";
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||||
|
|
||||||
@@ -11,10 +11,12 @@ import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
|||||||
*/
|
*/
|
||||||
export class ComfyBackendNode extends ComfyGraphNode {
|
export class ComfyBackendNode extends ComfyGraphNode {
|
||||||
comfyClass: string;
|
comfyClass: string;
|
||||||
|
displayName: string | null;
|
||||||
|
|
||||||
constructor(title: string, comfyClass: string, nodeData: any) {
|
constructor(title: string, comfyClass: string, nodeData: any) {
|
||||||
super(title)
|
super(title)
|
||||||
this.type = comfyClass; // XXX: workaround dependency in LGraphNode.addInput()
|
this.type = comfyClass; // XXX: workaround dependency in LGraphNode.addInput()
|
||||||
|
this.displayName = nodeData.display_name;
|
||||||
this.comfyClass = comfyClass;
|
this.comfyClass = comfyClass;
|
||||||
this.isBackendNode = true;
|
this.isBackendNode = true;
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// comfy class -> input name -> input config
|
||||||
private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {}
|
private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {}
|
||||||
|
|
||||||
private setup(nodeData: any) {
|
private setup(nodeData: any) {
|
||||||
@@ -43,7 +46,7 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
|||||||
ComfyBackendNode.defaultInputConfigs[this.type] = {}
|
ComfyBackendNode.defaultInputConfigs[this.type] = {}
|
||||||
|
|
||||||
for (const inputName in inputs) {
|
for (const inputName in inputs) {
|
||||||
const config = { minWidth: 1, minHeight: 1 };
|
const config: Partial<IComfyInputSlot> = {};
|
||||||
|
|
||||||
const inputData = inputs[inputName];
|
const inputData = inputs[inputName];
|
||||||
const type = inputData[0];
|
const type = inputData[0];
|
||||||
@@ -67,13 +70,14 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("widgetNodeType" in config)
|
if ("widgetNodeType" in config)
|
||||||
ComfyBackendNode.defaultInputConfigs[this.type][config.name] = config.config
|
ComfyBackendNode.defaultInputConfigs[this.type][inputName] = (config as IComfyInputSlot).config
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const o in nodeData["output"]) {
|
for (const o in nodeData["output"]) {
|
||||||
const output = nodeData["output"][o];
|
const output = nodeData["output"][o];
|
||||||
const outputName = nodeData["output_name"][o] || output;
|
const outputName = nodeData["output_name"][o] || output;
|
||||||
this.addOutput(outputName, output);
|
const outputShape = nodeData["output_is_list"][o] ? BuiltInSlotShape.GRID_SHAPE : BuiltInSlotShape.CIRCLE_SHAPE;
|
||||||
|
this.addOutput(outputName, output, { shape: outputShape });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.serialize_widgets = false;
|
this.serialize_widgets = false;
|
||||||
@@ -110,7 +114,7 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExecuted(outputData: GalleryOutput) {
|
override onExecuted(outputData: ComfyExecutionResult) {
|
||||||
console.warn("onExecuted outputs", outputData)
|
console.warn("onExecuted outputs", outputData)
|
||||||
this.triggerSlot(0, outputData)
|
this.triggerSlot(0, outputData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
|||||||
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
|
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
|
||||||
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
||||||
import type { SvelteComponentDev } from "svelte/internal";
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
|
import type { ComfyWidgetNode, ComfyExecutionResult, ComfyImageLocation } from "./ComfyWidgetNodes";
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
@@ -48,14 +48,14 @@ export default class ComfyGraphNode extends LGraphNode {
|
|||||||
* Triggered when the backend sends a finished output back with this node's ID.
|
* Triggered when the backend sends a finished output back with this node's ID.
|
||||||
* Valid for output nodes like SaveImage and PreviewImage.
|
* Valid for output nodes like SaveImage and PreviewImage.
|
||||||
*/
|
*/
|
||||||
onExecuted?(output: GalleryOutput): void;
|
onExecuted?(output: ComfyExecutionResult): void;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* When a prompt is queued, this will be called on the node if it can
|
* When a prompt is queued, this will be called on the node if it can
|
||||||
* provide any thumbnails for use with the prompt queue. Useful for HR Fix
|
* provide any thumbnails for use with the prompt queue. Useful for HR Fix
|
||||||
* or img2img workloads.
|
* or img2img workloads.
|
||||||
*/
|
*/
|
||||||
getPromptThumbnails?(): GalleryOutputEntry[] | null
|
getPromptThumbnails?(): ComfyImageLocation[] | null
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Allows you to manually specify an auto-config for certain input slot
|
* Allows you to manually specify an auto-config for certain input slot
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget, type SerializedLGraphNode } from "@litegraph-ts/core";
|
|
||||||
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
|
||||||
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
|
||||||
import { uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
|
||||||
|
|
||||||
export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties {
|
|
||||||
images: GalleryOutput | null,
|
|
||||||
index: number,
|
|
||||||
filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
|
|
||||||
genNumber: number,
|
|
||||||
updateMode: "replace" | "append"
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageCacheState = "none" | "uploading" | "failed" | "cached"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A node that can act as both an input and output image node by uploading
|
|
||||||
* the output file into ComfyUI's input folder.
|
|
||||||
*/
|
|
||||||
export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|
||||||
override properties: ComfyImageCacheNodeProperties = {
|
|
||||||
tags: [],
|
|
||||||
images: null,
|
|
||||||
index: 0,
|
|
||||||
filenames: {},
|
|
||||||
genNumber: 0,
|
|
||||||
updateMode: "replace"
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "images", type: "OUTPUT" },
|
|
||||||
{ name: "index", type: "number" },
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } },
|
|
||||||
{ name: "clear", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "filename", type: "string" },
|
|
||||||
{ name: "state", type: "string" },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
static propertyLayout: PropertyLayout = [
|
|
||||||
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
|
|
||||||
]
|
|
||||||
|
|
||||||
override saveUserState = false;
|
|
||||||
|
|
||||||
private _uploadPromise: Promise<void> | null = null;
|
|
||||||
|
|
||||||
stateWidget: ITextWidget;
|
|
||||||
filenameWidget: ITextWidget;
|
|
||||||
modeWidget: IComboWidget;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name)
|
|
||||||
this.stateWidget = this.addWidget<ITextWidget>(
|
|
||||||
"text",
|
|
||||||
"State",
|
|
||||||
"none"
|
|
||||||
);
|
|
||||||
this.stateWidget.disabled = true;
|
|
||||||
|
|
||||||
this.filenameWidget = this.addWidget<ITextWidget>(
|
|
||||||
"text",
|
|
||||||
"File",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
this.filenameWidget.disabled = true;
|
|
||||||
|
|
||||||
this.modeWidget = this.addWidget<IComboWidget>(
|
|
||||||
"combo",
|
|
||||||
"Mode",
|
|
||||||
this.properties.updateMode,
|
|
||||||
null,
|
|
||||||
{ property: "updateMode", values: ["replace", "append"] }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
|
||||||
if (property === "images") {
|
|
||||||
if (value != null)
|
|
||||||
this.properties.index = clamp(this.properties.index, 0, value.length)
|
|
||||||
else
|
|
||||||
this.properties.index = 0
|
|
||||||
}
|
|
||||||
else if (property === "updateMode") {
|
|
||||||
this.modeWidget.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateWidgets()
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateWidgets() {
|
|
||||||
if (this.properties.filenames && this.properties.images) {
|
|
||||||
const fileCount = this.properties.images.images.length;
|
|
||||||
const cachedCount = Object.keys(this.properties.filenames).length
|
|
||||||
console.warn(cachedCount, this.properties.filenames)
|
|
||||||
this.filenameWidget.value = `${fileCount} files, ${cachedCount} cached`
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.filenameWidget.value = `No files cached`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override onExecute() {
|
|
||||||
const index = this.getInputData(1)
|
|
||||||
if (typeof index === "number")
|
|
||||||
this.setIndex(index)
|
|
||||||
|
|
||||||
const existing = this.properties.filenames[this.properties.index]
|
|
||||||
let state = "none"
|
|
||||||
if (existing)
|
|
||||||
state = existing.status
|
|
||||||
|
|
||||||
this.stateWidget.value = state
|
|
||||||
|
|
||||||
let filename = null
|
|
||||||
if (this.properties.index in this.properties.filenames)
|
|
||||||
filename = this.properties.filenames[this.properties.index].filename
|
|
||||||
|
|
||||||
this.setOutputData(0, filename)
|
|
||||||
this.setOutputData(1, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
override stripUserState(o: SerializedLGraphNode) {
|
|
||||||
super.stripUserState(o);
|
|
||||||
o.properties.images = null
|
|
||||||
o.properties.index = 0
|
|
||||||
o.properties.filenames = {}
|
|
||||||
o.properties.genNumber = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private setIndex(newIndex: number, force: boolean = false) {
|
|
||||||
if (newIndex === this.properties.index && !force)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!this.properties.images || newIndex < 0 || newIndex >= this.properties.images.images.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setProperty("index", newIndex)
|
|
||||||
|
|
||||||
const data = this.properties.images.images[newIndex]
|
|
||||||
|
|
||||||
if (data == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.properties.filenames ||= {}
|
|
||||||
const existing = this.properties.filenames[newIndex]
|
|
||||||
|
|
||||||
if (existing != null && existing.status === "cached") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastGenNumber = this.properties.genNumber
|
|
||||||
|
|
||||||
// ComfyUI's LoadImage node only operates on files in its input
|
|
||||||
// folder. Usually we're dealing with an image in either the output
|
|
||||||
// folder (SaveImage) or the temp folder (PreviewImage). So we have
|
|
||||||
// to copy the image into ComfyUI's input folder first by using
|
|
||||||
// their upload API.
|
|
||||||
|
|
||||||
if (data.subfolder === "input") {
|
|
||||||
// Already in the correct folder for use by LoadImage
|
|
||||||
this.properties.filenames[newIndex] = { filename: data.filename, status: "cached" }
|
|
||||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.properties.filenames[newIndex] = { filename: null, status: "uploading" }
|
|
||||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
|
||||||
|
|
||||||
const promise = uploadImageToComfyUI(data)
|
|
||||||
.then((json: ComfyUploadImageAPIResponse) => {
|
|
||||||
console.debug("Gottem", json)
|
|
||||||
if (lastGenNumber === this.properties.genNumber) {
|
|
||||||
this.properties.filenames[newIndex] = { filename: json.name, status: "cached" }
|
|
||||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.warn("[ComfyImageCacheNode] New generation since index switched!")
|
|
||||||
}
|
|
||||||
this._uploadPromise = null;
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Error uploading:", e)
|
|
||||||
if (lastGenNumber === this.properties.genNumber) {
|
|
||||||
this.properties.filenames[newIndex] = { filename: null, status: "failed" }
|
|
||||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.warn("[ComfyImageCacheNode] New generation since index switched!")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this._uploadPromise)
|
|
||||||
this._uploadPromise.then(() => promise)
|
|
||||||
else
|
|
||||||
this._uploadPromise = promise
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override onAction(action: any, param: any) {
|
|
||||||
if (action === "clear") {
|
|
||||||
this.setProperty("images", null)
|
|
||||||
this.setProperty("filenames", {})
|
|
||||||
this.setProperty("index", 0)
|
|
||||||
this.updateWidgets();
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (param && "images" in param) {
|
|
||||||
this.setProperty("genNumber", this.properties.genNumber + 1)
|
|
||||||
|
|
||||||
const output = param as GalleryOutput;
|
|
||||||
|
|
||||||
if (this.properties.updateMode === "append" && this.properties.images != null) {
|
|
||||||
const newImages = this.properties.images.images.concat(output.images)
|
|
||||||
this.properties.images.images = newImages
|
|
||||||
this.setProperty("images", this.properties.images)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.setProperty("images", param as GalleryOutput)
|
|
||||||
this.setProperty("filenames", {})
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug("[ComfyImageCacheNode] Received output!", output, this.properties.updateMode, this.properties.images)
|
|
||||||
this.setIndex(0, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateWidgets();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyImageCacheNode,
|
|
||||||
title: "Comfy.ImageCache",
|
|
||||||
desc: "Allows reusing a previously output image by uploading it into ComfyUI's input folder.",
|
|
||||||
type: "image/cache"
|
|
||||||
})
|
|
||||||
33
src/lib/nodes/ComfyImageToFilepathNode.ts
Normal file
33
src/lib/nodes/ComfyImageToFilepathNode.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
import ComfyGraphNode from "./ComfyGraphNode";
|
||||||
|
import { comfyFileToAnnotatedFilepath, isComfyBoxImageMetadata, parseWhateverIntoImageMetadata } from "$lib/utils";
|
||||||
|
|
||||||
|
export default class ComfyImageToFilepathNode extends ComfyGraphNode {
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "image", type: "COMFYBOX_IMAGES,COMFYBOX_IMAGE" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "filepath", type: "string" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override onExecute() {
|
||||||
|
const data = this.getInputData(0)
|
||||||
|
const meta = parseWhateverIntoImageMetadata(data);
|
||||||
|
if (meta == null || meta.length === 0) {
|
||||||
|
this.setOutputData(0, null)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = comfyFileToAnnotatedFilepath(meta[0].comfyUIFile);
|
||||||
|
this.setOutputData(0, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyImageToFilepathNode,
|
||||||
|
title: "Comfy.ImageToFilepath",
|
||||||
|
desc: "Converts ComfyBox image metadata to an annotated filepath like \"image.png[output]\" for use with ComfyUI.",
|
||||||
|
type: "image/file_to_filepath"
|
||||||
|
})
|
||||||
35
src/lib/nodes/ComfyPickImageNode.ts
Normal file
35
src/lib/nodes/ComfyPickImageNode.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
import ComfyGraphNode from "./ComfyGraphNode";
|
||||||
|
import { isComfyBoxImageMetadataArray } from "$lib/utils";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: This is just a temporary workaround until litegraph can handle typed
|
||||||
|
* array arguments.
|
||||||
|
*/
|
||||||
|
export default class ComfyPickImageNode extends ComfyGraphNode {
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "images", type: "COMFYBOX_IMAGES" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "image", type: "COMFYBOX_IMAGE" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override onExecute() {
|
||||||
|
const data = this.getInputData(0)
|
||||||
|
if (data == null || !isComfyBoxImageMetadataArray(data)) {
|
||||||
|
this.setOutputData(0, null)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setOutputData(0, data[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyPickImageNode,
|
||||||
|
title: "Comfy.PickImage",
|
||||||
|
desc: "Picks out the first image from an array of ComfyBox images.",
|
||||||
|
type: "image/pick_image"
|
||||||
|
})
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget, NodeMode, type INumberWidget } from "@litegraph-ts/core";
|
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget, NodeMode, type INumberWidget, type UUID } from "@litegraph-ts/core";
|
||||||
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
||||||
import type { SvelteComponentDev } from "svelte/internal";
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import { writable, type Unsubscriber, type Writable, get } from "svelte/store";
|
import { writable, type Unsubscriber, type Writable, get } from "svelte/store";
|
||||||
import { clamp, convertComfyOutputToGradio, range } from "$lib/utils"
|
import { clamp, convertComfyOutputToGradio, range, type ComfyUploadImageType, isComfyBoxImageMetadata, filenameToComfyBoxMetadata, type ComfyBoxImageMetadata, isComfyExecutionResult, executionResultToImageMetadata, parseWhateverIntoImageMetadata } from "$lib/utils"
|
||||||
import layoutState from "$lib/stores/layoutState";
|
import layoutState from "$lib/stores/layoutState";
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
import queueState from "$lib/stores/queueState";
|
import queueState from "$lib/stores/queueState";
|
||||||
@@ -17,7 +17,8 @@ import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
|||||||
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
|
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
|
||||||
import RadioWidget from "$lib/widgets/RadioWidget.svelte";
|
import RadioWidget from "$lib/widgets/RadioWidget.svelte";
|
||||||
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
|
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
|
||||||
import ImageCompareWidget from "$lib/widgets/ImageCompareWidget.svelte";
|
import ImageEditorWidget from "$lib/widgets/ImageEditorWidget.svelte";
|
||||||
|
import type { NodeID } from "$lib/api";
|
||||||
|
|
||||||
export type AutoConfigOptions = {
|
export type AutoConfigOptions = {
|
||||||
includeProperties?: Set<string> | null,
|
includeProperties?: Set<string> | null,
|
||||||
@@ -33,7 +34,7 @@ export type AutoConfigOptions = {
|
|||||||
* - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant"
|
* - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant"
|
||||||
* attribute and set `validNodeTypes` to the type of the litegraph node
|
* attribute and set `validNodeTypes` to the type of the litegraph node
|
||||||
* - Add a new entry in the `values` array, like "knob" or "dial" for ComfySliderWidget
|
* - Add a new entry in the `values` array, like "knob" or "dial" for ComfySliderWidget
|
||||||
* - Add an {#if widget.attrs.variant === <...>} statement in the corresponding Svelte component
|
* - Add an {#if widget.attrs.variant === <...>} statement in the existing Svelte component
|
||||||
*
|
*
|
||||||
* Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame!
|
* Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame!
|
||||||
* You will have to either implement an internal delay on the event triggering
|
* You will have to either implement an internal delay on the event triggering
|
||||||
@@ -91,7 +92,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
|
|
||||||
// TODO these are bad, create override methods instead
|
// TODO these are bad, create override methods instead
|
||||||
// input slots
|
// input slots
|
||||||
inputIndex: number = 0;
|
inputIndex: number | null = null;
|
||||||
storeActionName: string | null = "store";
|
storeActionName: string | null = "store";
|
||||||
|
|
||||||
// output slots
|
// output slots
|
||||||
@@ -169,6 +170,10 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
|
|
||||||
parseValue(value: any): T { return value as T };
|
parseValue(value: any): T { return value as T };
|
||||||
|
|
||||||
|
getValue(): T {
|
||||||
|
return get(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
setValue(value: any, noChangedEvent: boolean = false) {
|
setValue(value: any, noChangedEvent: boolean = false) {
|
||||||
if (noChangedEvent)
|
if (noChangedEvent)
|
||||||
this._noChangedEvent = true;
|
this._noChangedEvent = true;
|
||||||
@@ -193,7 +198,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
* Logic to run if this widget can be treated as output (slider, combo, text)
|
* Logic to run if this widget can be treated as output (slider, combo, text)
|
||||||
*/
|
*/
|
||||||
override onExecute(param: any, options: object) {
|
override onExecute(param: any, options: object) {
|
||||||
if (this.copyFromInputLink) {
|
if (this.inputIndex != null) {
|
||||||
if (this.inputs.length >= this.inputIndex) {
|
if (this.inputs.length >= this.inputIndex) {
|
||||||
const data = this.getInputData(this.inputIndex)
|
const data = this.getInputData(this.inputIndex)
|
||||||
if (data != null) { // TODO can "null" be a legitimate value here?
|
if (data != null) { // TODO can "null" be a legitimate value here?
|
||||||
@@ -201,8 +206,10 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.outputs.length >= this.outputIndex) {
|
if (this.outputIndex != null) {
|
||||||
this.setOutputData(this.outputIndex, get(this.value))
|
if (this.outputs.length >= this.outputIndex) {
|
||||||
|
this.setOutputData(this.outputIndex, get(this.value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const propName in this.shownOutputProperties) {
|
for (const propName in this.shownOutputProperties) {
|
||||||
const data = this.shownOutputProperties[propName]
|
const data = this.shownOutputProperties[propName]
|
||||||
@@ -265,7 +272,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.setWidgetTitle) {
|
if (options.setWidgetTitle) {
|
||||||
const widget = layoutState.findLayoutForNode(this.id)
|
const widget = layoutState.findLayoutForNode(this.id as NodeID)
|
||||||
if (widget && input.name !== "") {
|
if (widget && input.name !== "") {
|
||||||
widget.attrs.title = input.name;
|
widget.attrs.title = input.name;
|
||||||
}
|
}
|
||||||
@@ -284,7 +291,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notifyPropsChanged() {
|
notifyPropsChanged() {
|
||||||
const layoutEntry = layoutState.findLayoutEntryForNode(this.id)
|
const layoutEntry = layoutState.findLayoutEntryForNode(this.id as NodeID)
|
||||||
if (layoutEntry && layoutEntry.parent) {
|
if (layoutEntry && layoutEntry.parent) {
|
||||||
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
||||||
}
|
}
|
||||||
@@ -596,15 +603,21 @@ LiteGraph.registerNodeType({
|
|||||||
})
|
})
|
||||||
|
|
||||||
/** Raw output as received from ComfyUI's backend */
|
/** Raw output as received from ComfyUI's backend */
|
||||||
export type GalleryOutput = {
|
export interface ComfyExecutionResult {
|
||||||
images: GalleryOutputEntry[]
|
// Technically this response can contain arbitrary data, but "images" is the
|
||||||
|
// most frequently used as it's output by LoadImage and PreviewImage, the
|
||||||
|
// only two output nodes in base ComfyUI.
|
||||||
|
images: ComfyImageLocation[] | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Raw output entry as received from ComfyUI's backend */
|
/** Raw output entry as received from ComfyUI's backend */
|
||||||
export type GalleryOutputEntry = {
|
export type ComfyImageLocation = {
|
||||||
|
/* Filename with extension in the subfolder. */
|
||||||
filename: string,
|
filename: string,
|
||||||
|
/* Subfolder in the containing folder. */
|
||||||
subfolder: string,
|
subfolder: string,
|
||||||
type: string
|
/* Base ComfyUI folder where the image is located. */
|
||||||
|
type: ComfyUploadImageType
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
||||||
@@ -612,7 +625,7 @@ export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
|||||||
updateMode: "replace" | "append",
|
updateMode: "replace" | "append",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
export class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
||||||
override properties: ComfyGalleryProperties = {
|
override properties: ComfyGalleryProperties = {
|
||||||
tags: [],
|
tags: [],
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
@@ -623,14 +636,11 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
|||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: "images", type: "OUTPUT" },
|
{ name: "images", type: "OUTPUT" },
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } },
|
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } }
|
||||||
{ name: "clear", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
],
|
||||||
outputs: [
|
outputs: [
|
||||||
|
{ name: "images", type: "COMFYBOX_IMAGES" },
|
||||||
{ name: "selected_index", type: "number" },
|
{ name: "selected_index", type: "number" },
|
||||||
{ name: "width", type: "number" },
|
|
||||||
{ name: "height", type: "number" },
|
|
||||||
{ name: "filename", type: "string" },
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,7 +650,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
|||||||
|
|
||||||
override svelteComponentType = GalleryWidget
|
override svelteComponentType = GalleryWidget
|
||||||
override defaultValue = []
|
override defaultValue = []
|
||||||
override copyFromInputLink = false;
|
override inputIndex = null;
|
||||||
override saveUserState = false;
|
override saveUserState = false;
|
||||||
override outputIndex = null;
|
override outputIndex = null;
|
||||||
override changedIndex = null;
|
override changedIndex = null;
|
||||||
@@ -663,53 +673,30 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imageSize: Vector2 = [1, 1]
|
|
||||||
|
|
||||||
override onExecute() {
|
override onExecute() {
|
||||||
const index = this.properties.index;
|
this.setOutputData(0, get(this.value))
|
||||||
|
this.setOutputData(1, this.properties.index)
|
||||||
this.setOutputData(0, index)
|
|
||||||
this.setOutputData(1, this.imageSize[0])
|
|
||||||
this.setOutputData(2, this.imageSize[1])
|
|
||||||
|
|
||||||
let filename: string | null = null;
|
|
||||||
if (index != null) {
|
|
||||||
const entry = get(this.value)[index];
|
|
||||||
if (entry)
|
|
||||||
filename = entry.name
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setOutputData(3, filename)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override onAction(action: any, param: any, options: { action_call?: string }) {
|
override onAction(action: any, param: any, options: { action_call?: string }) {
|
||||||
super.onAction(action, param, options)
|
super.onAction(action, param, options)
|
||||||
|
|
||||||
if (action === "clear") {
|
|
||||||
this.setValue([])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override formatValue(value: GradioFileData[] | null): string {
|
override formatValue(value: ComfyBoxImageMetadata[] | null): string {
|
||||||
return `Images: ${value?.length || 0}`
|
return `Images: ${value?.length || 0}`
|
||||||
}
|
}
|
||||||
|
|
||||||
override parseValue(param: any): GradioFileData[] {
|
override parseValue(param: any): ComfyBoxImageMetadata[] {
|
||||||
if (!(typeof param === "object" && "images" in param)) {
|
const meta = parseWhateverIntoImageMetadata(param) || [];
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = param as GalleryOutput
|
console.debug("[ComfyGalleryNode] Received output!", param)
|
||||||
console.debug("[ComfyGalleryNode] Received output!", data)
|
|
||||||
|
|
||||||
const galleryItems: GradioFileData[] = convertComfyOutputToGradio(data)
|
|
||||||
|
|
||||||
if (this.properties.updateMode === "append") {
|
if (this.properties.updateMode === "append") {
|
||||||
const currentValue = get(this.value)
|
const currentValue = get(this.value)
|
||||||
return currentValue.concat(galleryItems)
|
return currentValue.concat(meta)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return galleryItems;
|
return meta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,15 +873,13 @@ LiteGraph.registerNodeType({
|
|||||||
type: "ui/radio"
|
type: "ui/radio"
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface ComfyImageUploadProperties extends ComfyWidgetProperties {
|
export interface ComfyImageEditorNodeProperties extends ComfyWidgetProperties {
|
||||||
fileCount: "single" | "multiple" // gradio File component format
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComfyImageUploadNode extends ComfyWidgetNode<Array<GradioFileData>> {
|
export class ComfyImageEditorNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
||||||
override properties: ComfyImageUploadProperties = {
|
override properties: ComfyImageEditorNodeProperties = {
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
fileCount: "single",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
@@ -902,55 +887,25 @@ export class ComfyImageUploadNode extends ComfyWidgetNode<Array<GradioFileData>>
|
|||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
],
|
],
|
||||||
outputs: [
|
outputs: [
|
||||||
{ name: "filename", type: "string" }, // TODO support batches
|
{ name: "images", type: "COMFYBOX_IMAGES" }, // TODO support batches
|
||||||
{ name: "width", type: "number" },
|
|
||||||
{ name: "height", type: "number" },
|
|
||||||
{ name: "image_count", type: "number" },
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
override svelteComponentType = ImageUploadWidget;
|
override svelteComponentType = ImageEditorWidget;
|
||||||
override defaultValue = [];
|
override defaultValue = [];
|
||||||
override outputIndex = null;
|
override outputIndex = 0;
|
||||||
override changedIndex = 3;
|
override inputIndex = null;
|
||||||
|
override changedIndex = 1;
|
||||||
override storeActionName = "store";
|
override storeActionName = "store";
|
||||||
override saveUserState = false;
|
override saveUserState = false;
|
||||||
|
|
||||||
imageSize: Vector2 = [1, 1];
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
constructor(name?: string) {
|
||||||
super(name, [])
|
super(name, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
override parseValue(value: any): GradioFileData[] {
|
override parseValue(value: any): ComfyBoxImageMetadata[] {
|
||||||
if (value == null)
|
return parseWhateverIntoImageMetadata(value) || [];
|
||||||
return []
|
|
||||||
|
|
||||||
if (typeof value === "string" && value !== "") { // Single filename
|
|
||||||
return [{ name: value, data: value, orig_name: value, is_file: true }]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override onExecute(param: any, options: object) {
|
|
||||||
super.onExecute(param, options);
|
|
||||||
|
|
||||||
const value = get(this.value)
|
|
||||||
if (value.length > 0 && value[0].name) {
|
|
||||||
this.setOutputData(0, value[0].name) // TODO when ComfyUI LoadImage supports loading an image batch
|
|
||||||
this.setOutputData(1, this.imageSize[0])
|
|
||||||
this.setOutputData(2, this.imageSize[1])
|
|
||||||
this.setOutputData(3, value.length)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.setOutputData(0, "")
|
|
||||||
this.setOutputData(1, 1)
|
|
||||||
this.setOutputData(2, 1)
|
|
||||||
this.setOutputData(3, 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override formatValue(value: GradioFileData[]): string {
|
override formatValue(value: GradioFileData[]): string {
|
||||||
@@ -959,97 +914,8 @@ export class ComfyImageUploadNode extends ComfyWidgetNode<Array<GradioFileData>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
LiteGraph.registerNodeType({
|
||||||
class: ComfyImageUploadNode,
|
class: ComfyImageEditorNode,
|
||||||
title: "UI.ImageUpload",
|
title: "UI.ImageEditor",
|
||||||
desc: "Widget that lets you upload images into ComfyUI's input folder",
|
desc: "Widget that lets you upload and edit a multi-layered image. Can also act like a standalone image uploader.",
|
||||||
type: "ui/image_upload"
|
type: "ui/image_editor"
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyImageCompareNodeProperties extends ComfyWidgetProperties {
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FileNameOrGalleryData = string | GalleryOutputEntry;
|
|
||||||
export type ImageCompareData = [FileNameOrGalleryData, FileNameOrGalleryData]
|
|
||||||
|
|
||||||
export class ComfyImageCompareNode extends ComfyWidgetNode<ImageCompareData> {
|
|
||||||
override properties: ComfyImageCompareNodeProperties = {
|
|
||||||
defaultValue: [],
|
|
||||||
tags: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION },
|
|
||||||
{ name: "left_image", type: "string" },
|
|
||||||
{ name: "right_image", type: "string" },
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = ImageCompareWidget;
|
|
||||||
override defaultValue: ImageCompareData = ["", ""];
|
|
||||||
override outputIndex = null;
|
|
||||||
override changedIndex = 3;
|
|
||||||
override storeActionName = "store";
|
|
||||||
override saveUserState = false;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, ["", ""])
|
|
||||||
}
|
|
||||||
|
|
||||||
override onExecute() {
|
|
||||||
const valueA = this.getInputData(1)
|
|
||||||
const valueB = this.getInputData(2)
|
|
||||||
let current = get(this.value)
|
|
||||||
let changed = false;
|
|
||||||
if (valueA != null && current[0] != valueA) {
|
|
||||||
current[0] = valueA
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (valueB != null && current[1] != valueB) {
|
|
||||||
current[1] = valueB
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (changed)
|
|
||||||
this.value.set(current)
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(value: any): ImageCompareData {
|
|
||||||
if (value == null) {
|
|
||||||
return ["", ""]
|
|
||||||
}
|
|
||||||
else if (typeof value === "string" && value !== "") { // Single filename
|
|
||||||
const prevValue = get(this.value)
|
|
||||||
prevValue.push(value)
|
|
||||||
if (prevValue.length > 2)
|
|
||||||
prevValue.splice(0, 1)
|
|
||||||
return prevValue as ImageCompareData
|
|
||||||
}
|
|
||||||
else if (typeof value === "object" && "images" in value && value.images.length > 0) {
|
|
||||||
const output = value as GalleryOutput
|
|
||||||
const prevValue = get(this.value)
|
|
||||||
prevValue.push(output.images[0].filename)
|
|
||||||
if (prevValue.length > 2)
|
|
||||||
prevValue.splice(0, 1)
|
|
||||||
return prevValue as ImageCompareData
|
|
||||||
}
|
|
||||||
else if (Array.isArray(value) && typeof value[0] === "string" && typeof value[1] === "string") {
|
|
||||||
return value as ImageCompareData
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return ["", ""]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override formatValue(value: GradioFileData[]): string {
|
|
||||||
return `Images: ${value?.length || 0}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyImageCompareNode,
|
|
||||||
title: "UI.ImageCompare",
|
|
||||||
desc: "Widget that lets you compare two images",
|
|
||||||
type: "ui/image_compare"
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export {
|
|||||||
export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode"
|
export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode"
|
||||||
export { default as ComfyValueControl } from "./ComfyValueControl"
|
export { default as ComfyValueControl } from "./ComfyValueControl"
|
||||||
export { default as ComfySelector } from "./ComfySelector"
|
export { default as ComfySelector } from "./ComfySelector"
|
||||||
export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode"
|
|
||||||
export { default as ComfyTriggerNewEventNode } from "./ComfyTriggerNewEventNode"
|
export { default as ComfyTriggerNewEventNode } from "./ComfyTriggerNewEventNode"
|
||||||
export { default as ComfyConfigureQueuePromptButton } from "./ComfyConfigureQueuePromptButton"
|
export { default as ComfyConfigureQueuePromptButton } from "./ComfyConfigureQueuePromptButton"
|
||||||
|
export { default as ComfyPickImageNode } from "./ComfyPickImageNode"
|
||||||
|
export { default as ComfyImageToFilepathNode } from "./ComfyImageToFilepathNode"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { type LGraphNode, type IWidget, type LGraph, NodeMode, type LGraphRemove
|
|||||||
import { SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
import { SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
import type { ComfyWidgetNode } from '$lib/nodes';
|
import type { ComfyWidgetNode } from '$lib/nodes';
|
||||||
import type { NodeID } from '$lib/api';
|
import type { NodeID } from '$lib/api';
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
type DragItemEntry = {
|
type DragItemEntry = {
|
||||||
/*
|
/*
|
||||||
@@ -61,11 +62,6 @@ export type LayoutState = {
|
|||||||
*/
|
*/
|
||||||
allItemsByNode: Record<NodeID, DragItemEntry>,
|
allItemsByNode: Record<NodeID, DragItemEntry>,
|
||||||
|
|
||||||
/*
|
|
||||||
* Next ID to use for instantiating a new drag item
|
|
||||||
*/
|
|
||||||
currentId: UUID,
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Selected drag items.
|
* Selected drag items.
|
||||||
*/
|
*/
|
||||||
@@ -411,6 +407,18 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
defaultValue: ""
|
defaultValue: ""
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Editor
|
||||||
|
{
|
||||||
|
name: "variant",
|
||||||
|
type: "enum",
|
||||||
|
location: "widget",
|
||||||
|
editable: true,
|
||||||
|
validNodeTypes: ["ui/image_editor"],
|
||||||
|
values: ["inlineEditor", "fileUpload"],
|
||||||
|
defaultValue: "inlineEditor",
|
||||||
|
refreshPanelOnChange: true
|
||||||
|
},
|
||||||
|
|
||||||
// Gallery
|
// Gallery
|
||||||
{
|
{
|
||||||
name: "variant",
|
name: "variant",
|
||||||
@@ -668,7 +676,6 @@ const store: Writable<LayoutState> = writable({
|
|||||||
root: null,
|
root: null,
|
||||||
allItems: {},
|
allItems: {},
|
||||||
allItemsByNode: {},
|
allItemsByNode: {},
|
||||||
currentId: 0,
|
|
||||||
currentSelection: [],
|
currentSelection: [],
|
||||||
currentSelectionNodes: [],
|
currentSelectionNodes: [],
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
@@ -703,7 +710,7 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
|
|||||||
const state = get(store);
|
const state = get(store);
|
||||||
const dragItem: ContainerLayout = {
|
const dragItem: ContainerLayout = {
|
||||||
type: "container",
|
type: "container",
|
||||||
id: `${state.currentId++}`,
|
id: uuidv4(),
|
||||||
attrsChanged: writable(0),
|
attrsChanged: writable(0),
|
||||||
attrs: {
|
attrs: {
|
||||||
...defaultWidgetAttributes,
|
...defaultWidgetAttributes,
|
||||||
@@ -726,7 +733,7 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
|
|||||||
const widgetName = "Widget"
|
const widgetName = "Widget"
|
||||||
const dragItem: WidgetLayout = {
|
const dragItem: WidgetLayout = {
|
||||||
type: "widget",
|
type: "widget",
|
||||||
id: `${state.currentId++}`,
|
id: uuidv4(),
|
||||||
node: node,
|
node: node,
|
||||||
attrsChanged: writable(0),
|
attrsChanged: writable(0),
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -939,7 +946,6 @@ function initDefaultLayout() {
|
|||||||
root: null,
|
root: null,
|
||||||
allItems: {},
|
allItems: {},
|
||||||
allItemsByNode: {},
|
allItemsByNode: {},
|
||||||
currentId: 0,
|
|
||||||
currentSelection: [],
|
currentSelection: [],
|
||||||
currentSelectionNodes: [],
|
currentSelectionNodes: [],
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
@@ -964,7 +970,6 @@ function initDefaultLayout() {
|
|||||||
export type SerializedLayoutState = {
|
export type SerializedLayoutState = {
|
||||||
root: DragItemID | null,
|
root: DragItemID | null,
|
||||||
allItems: Record<DragItemID, SerializedDragEntry>,
|
allItems: Record<DragItemID, SerializedDragEntry>,
|
||||||
currentId: UUID,
|
|
||||||
attrs: LayoutAttributes
|
attrs: LayoutAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1002,7 +1007,6 @@ function serialize(): SerializedLayoutState {
|
|||||||
return {
|
return {
|
||||||
root: state.root?.id,
|
root: state.root?.id,
|
||||||
allItems,
|
allItems,
|
||||||
currentId: state.currentId,
|
|
||||||
attrs: state.attrs
|
attrs: state.attrs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1055,7 +1059,6 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
|||||||
root,
|
root,
|
||||||
allItems,
|
allItems,
|
||||||
allItemsByNode,
|
allItemsByNode,
|
||||||
currentId: data.currentId,
|
|
||||||
currentSelection: [],
|
currentSelection: [],
|
||||||
currentSelectionNodes: [],
|
currentSelectionNodes: [],
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, NodeID, PromptID } from "$lib/api";
|
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, NodeID, PromptID } from "$lib/api";
|
||||||
import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp";
|
import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp";
|
||||||
import type { GalleryOutput } from "$lib/nodes/ComfyWidgetNodes";
|
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
import notify from "$lib/notify";
|
import notify from "$lib/notify";
|
||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ type QueueStateOps = {
|
|||||||
executionError: (promptID: PromptID, message: string) => void,
|
executionError: (promptID: PromptID, message: string) => void,
|
||||||
progressUpdated: (progress: Progress) => void
|
progressUpdated: (progress: Progress) => void
|
||||||
afterQueued: (promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
|
afterQueued: (promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
|
||||||
onExecuted: (promptID: PromptID, nodeID: NodeID, output: GalleryOutput) => void
|
onExecuted: (promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueueEntry = {
|
export type QueueEntry = {
|
||||||
@@ -257,7 +257,7 @@ function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromp
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExecuted(promptID: PromptID, nodeID: NodeID, output: GalleryOutput) {
|
function onExecuted(promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) {
|
||||||
console.debug("[queueState] onExecuted", promptID, nodeID, output)
|
console.debug("[queueState] onExecuted", promptID, nodeID, output)
|
||||||
store.update(s => {
|
store.update(s => {
|
||||||
const [index, entry, queue] = findEntryInPending(promptID)
|
const [index, entry, queue] = findEntryInPending(promptID)
|
||||||
|
|||||||
193
src/lib/utils.ts
193
src/lib/utils.ts
@@ -6,7 +6,7 @@ import { get } from "svelte/store"
|
|||||||
import layoutState from "$lib/stores/layoutState"
|
import layoutState from "$lib/stores/layoutState"
|
||||||
import type { SvelteComponentDev } from "svelte/internal";
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
import type { SerializedLGraph } from "@litegraph-ts/core";
|
import type { SerializedLGraph } from "@litegraph-ts/core";
|
||||||
import type { GalleryOutput, GalleryOutputEntry } from "./nodes/ComfyWidgetNodes";
|
import type { FileNameOrGalleryData, ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes";
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
|
|
||||||
export function clamp(n: number, min: number, max: number): number {
|
export function clamp(n: number, min: number, max: number): number {
|
||||||
@@ -125,26 +125,48 @@ export const debounce = (callback: Function, wait = 250) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileData[] {
|
export function convertComfyOutputToGradio(output: ComfyExecutionResult): GradioFileData[] {
|
||||||
return output.images.map(r => {
|
return output.images.map(convertComfyOutputEntryToGradio);
|
||||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
|
||||||
const params = new URLSearchParams(r)
|
|
||||||
const fileData: GradioFileData = {
|
|
||||||
name: r.filename,
|
|
||||||
orig_name: r.filename,
|
|
||||||
is_file: false,
|
|
||||||
data: url + "/view?" + params
|
|
||||||
}
|
|
||||||
return fileData
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertComfyOutputToComfyURL(output: GalleryOutputEntry): string {
|
export function convertComfyOutputEntryToGradio(r: ComfyImageLocation): GradioFileData {
|
||||||
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
const params = new URLSearchParams(r)
|
||||||
|
const fileData: GradioFileData = {
|
||||||
|
name: r.filename,
|
||||||
|
orig_name: r.filename,
|
||||||
|
is_file: false,
|
||||||
|
data: url + "/view?" + params
|
||||||
|
}
|
||||||
|
return fileData
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertComfyOutputToComfyURL(output: FileNameOrGalleryData): string {
|
||||||
|
if (typeof output === "string")
|
||||||
|
return output;
|
||||||
|
|
||||||
const params = new URLSearchParams(output)
|
const params = new URLSearchParams(output)
|
||||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
return url + "/view?" + params
|
return url + "/view?" + params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertGradioFileDataToComfyURL(image: GradioFileData, type: ComfyUploadImageType = "input"): string {
|
||||||
|
const baseUrl = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
const params = new URLSearchParams({ filename: image.name, subfolder: "", type })
|
||||||
|
return `${baseUrl}/view?${params}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertGradioFileDataToComfyOutput(fileData: GradioFileData, type: ComfyUploadImageType = "input"): ComfyImageLocation {
|
||||||
|
if (!fileData.is_file)
|
||||||
|
throw "Can't convert blob data to comfy output!"
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: fileData.name,
|
||||||
|
subfolder: "",
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function convertFilenameToComfyURL(filename: string,
|
export function convertFilenameToComfyURL(filename: string,
|
||||||
subfolder: string = "",
|
subfolder: string = "",
|
||||||
type: "input" | "output" | "temp" = "output"): string {
|
type: "input" | "output" | "temp" = "output"): string {
|
||||||
@@ -171,26 +193,137 @@ export function jsonToJsObject(json: string): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ComfyUploadImageType = "output" | "input" | "temp"
|
||||||
|
|
||||||
export interface ComfyUploadImageAPIResponse {
|
export interface ComfyUploadImageAPIResponse {
|
||||||
name: string
|
name: string, // Yes this is different from the "executed" event args
|
||||||
|
subfolder: string,
|
||||||
|
type: ComfyUploadImageType
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadImageToComfyUI(data: GalleryOutputEntry): Promise<ComfyUploadImageAPIResponse> {
|
/*
|
||||||
|
* Uploads an image into ComfyUI's `input` folder.
|
||||||
|
*/
|
||||||
|
export async function uploadImageToComfyUI(blob: Blob, filename: string, type: ComfyUploadImageType, subfolder: string = "", overwrite: boolean = false): Promise<ComfyImageLocation> {
|
||||||
|
console.debug("[utils] Uploading image to ComfyUI", filename, blob.size)
|
||||||
|
|
||||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
const params = new URLSearchParams(data)
|
|
||||||
|
|
||||||
return fetch(url + "/view?" + params)
|
const formData = new FormData();
|
||||||
.then((r) => r.blob())
|
formData.append("image", blob, filename);
|
||||||
.then((blob) => {
|
formData.set("type", type)
|
||||||
console.debug("Fetchin", url, params)
|
formData.set("subfolder", subfolder)
|
||||||
const formData = new FormData();
|
formData.set("overwrite", String(overwrite))
|
||||||
formData.append("image", blob, data.filename);
|
|
||||||
return fetch(
|
const req = new Request(url + "/upload/image", {
|
||||||
new Request(url + "/upload/image", {
|
body: formData,
|
||||||
body: formData,
|
method: 'POST'
|
||||||
method: 'POST'
|
});
|
||||||
})
|
|
||||||
)
|
return fetch(req)
|
||||||
})
|
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
|
.then((resp) => {
|
||||||
|
return {
|
||||||
|
filename: resp.name,
|
||||||
|
subfolder: resp.subfolder,
|
||||||
|
type: resp.type
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convenient type for passing around image filepaths and their metadata with
|
||||||
|
* wires. Needs to be converted to a filename for use with LoadImage.
|
||||||
|
*
|
||||||
|
* Litegraph type is COMFYBOX_IMAGE. The array type is COMFYBOX_IMAGES.
|
||||||
|
*/
|
||||||
|
export type ComfyBoxImageMetadata = {
|
||||||
|
/* For easy structural type detection */
|
||||||
|
isComfyBoxImageMetadata: true,
|
||||||
|
/* Pointer to where this image resides in ComfyUI. */
|
||||||
|
comfyUIFile: ComfyImageLocation,
|
||||||
|
/* Readable name of the image. */
|
||||||
|
name: string
|
||||||
|
/* Tags applicable to this image, like ["mask"]. */
|
||||||
|
tags: string[],
|
||||||
|
/* Image width. */
|
||||||
|
width?: number,
|
||||||
|
/* Image height. */
|
||||||
|
height?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isComfyBoxImageMetadata(value: any): value is ComfyBoxImageMetadata {
|
||||||
|
return value && typeof value === "object" && (value as any).isComfyBoxImageMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isComfyBoxImageMetadataArray(value: any): value is ComfyBoxImageMetadata[] {
|
||||||
|
return Array.isArray(value) && value.every(isComfyBoxImageMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isComfyExecutionResult(value: any): value is ComfyExecutionResult {
|
||||||
|
return value && typeof value === "object" && Array.isArray(value.images)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filenameToComfyBoxMetadata(filename: string, type: ComfyUploadImageType, subfolder: string = ""): ComfyBoxImageMetadata {
|
||||||
|
return {
|
||||||
|
isComfyBoxImageMetadata: true,
|
||||||
|
comfyUIFile: {
|
||||||
|
filename,
|
||||||
|
subfolder,
|
||||||
|
type
|
||||||
|
},
|
||||||
|
name: "Filename",
|
||||||
|
tags: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function comfyFileToComfyBoxMetadata(comfyUIFile: ComfyImageLocation): ComfyBoxImageMetadata {
|
||||||
|
return {
|
||||||
|
isComfyBoxImageMetadata: true,
|
||||||
|
comfyUIFile,
|
||||||
|
name: "File",
|
||||||
|
tags: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Converts a ComfyUI file into an annotated filepath. Backend nodes like
|
||||||
|
* LoadImage support syntax like "subfolder/image.png [output]" to specify which
|
||||||
|
* image folder to load from.
|
||||||
|
*/
|
||||||
|
export function comfyFileToAnnotatedFilepath(comfyUIFile: ComfyImageLocation): string {
|
||||||
|
let path = ""
|
||||||
|
if (comfyUIFile.subfolder != "")
|
||||||
|
path = comfyUIFile.subfolder + "/";
|
||||||
|
|
||||||
|
path += `${comfyUIFile.filename} [${comfyUIFile.type}]`
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function executionResultToImageMetadata(result: ComfyExecutionResult): ComfyBoxImageMetadata[] {
|
||||||
|
return result.images.map(comfyFileToComfyBoxMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWhateverIntoImageMetadata(param: any): ComfyBoxImageMetadata[] | null {
|
||||||
|
let meta: ComfyBoxImageMetadata[] | null = null
|
||||||
|
|
||||||
|
if (isComfyBoxImageMetadata(param)) {
|
||||||
|
meta = [param];
|
||||||
|
}
|
||||||
|
else if (Array.isArray(param) && param.every(isComfyBoxImageMetadata)) {
|
||||||
|
meta = param
|
||||||
|
}
|
||||||
|
else if (isComfyExecutionResult(param)) {
|
||||||
|
meta = executionResultToImageMetadata(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function comfyBoxImageToComfyFile(image: ComfyBoxImageMetadata): ComfyImageLocation {
|
||||||
|
return image.comfyUIFile
|
||||||
|
}
|
||||||
|
|
||||||
|
export function comfyBoxImageToComfyURL(image: ComfyBoxImageMetadata): string {
|
||||||
|
return convertComfyOutputToComfyURL(image.comfyUIFile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
|
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||||
import { clamp } from "$lib/utils";
|
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||||
import { f7 } from "framework7-svelte";
|
import { f7 } from "framework7-svelte";
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
let node: ComfyGalleryNode | null = null;
|
let node: ComfyGalleryNode | null = null;
|
||||||
let nodeValue: Writable<GradioFileData[]> | null = null;
|
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||||
let propsChanged: Writable<number> | null = null;
|
let propsChanged: Writable<number> | null = null;
|
||||||
let option: number | null = null;
|
let option: number | null = null;
|
||||||
let imageWidth: number = 1;
|
let imageWidth: number = 1;
|
||||||
@@ -154,8 +154,10 @@
|
|||||||
<div class="wrapper comfy-image-widget" style={widget.attrs.style || ""} bind:this={element}>
|
<div class="wrapper comfy-image-widget" style={widget.attrs.style || ""} bind:this={element}>
|
||||||
<Block variant="solid" padding={false}>
|
<Block variant="solid" padding={false}>
|
||||||
{#if $nodeValue && $nodeValue.length > 0}
|
{#if $nodeValue && $nodeValue.length > 0}
|
||||||
|
{@const value = $nodeValue[$nodeValue.length-1]}
|
||||||
|
{@const url = comfyBoxImageToComfyURL(value)}
|
||||||
<StaticImage
|
<StaticImage
|
||||||
value={$nodeValue[$nodeValue.length-1].data}
|
value={url}
|
||||||
show_label={widget.attrs.title != ""}
|
show_label={widget.attrs.title != ""}
|
||||||
label={widget.attrs.title}
|
label={widget.attrs.title}
|
||||||
bind:imageWidth
|
bind:imageWidth
|
||||||
@@ -167,11 +169,12 @@
|
|||||||
</Block>
|
</Block>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
{@const images = $nodeValue.map(comfyBoxImageToComfyURL)}
|
||||||
<div class="wrapper comfy-gallery-widget gradio-gallery" style={widget.attrs.style || ""} bind:this={element}>
|
<div class="wrapper comfy-gallery-widget gradio-gallery" style={widget.attrs.style || ""} bind:this={element}>
|
||||||
<Block variant="solid" padding={false}>
|
<Block variant="solid" padding={false}>
|
||||||
<div class="padding">
|
<div class="padding">
|
||||||
<Gallery
|
<Gallery
|
||||||
bind:value={$nodeValue}
|
value={images}
|
||||||
label={widget.attrs.title}
|
label={widget.attrs.title}
|
||||||
show_label={widget.attrs.title !== ""}
|
show_label={widget.attrs.title !== ""}
|
||||||
{style}
|
{style}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
|
||||||
import { Block } from "@gradio/atoms";
|
|
||||||
import ImageComparison from "$lib/components/ImageComparison.svelte";
|
|
||||||
import { get, type Writable, writable } from "svelte/store";
|
|
||||||
import { isDisabled } from "./utils"
|
|
||||||
import type { SelectData } from "@gradio/utils";
|
|
||||||
import type { ComfyImageCompareNode, ImageCompareData } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import { convertFilenameToComfyURL } from "$lib/utils";
|
|
||||||
import { TabItem, Tabs } from "@gradio/tabs";
|
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
|
||||||
export let isMobile: boolean = false;
|
|
||||||
let node: ComfyImageCompareNode | null = null;
|
|
||||||
let nodeValue: Writable<ImageCompareData> | null = null;
|
|
||||||
let attrsChanged: Writable<number> | null = null;
|
|
||||||
let leftUrl: string = ""
|
|
||||||
let rightUrl: string = ""
|
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
|
||||||
|
|
||||||
function setNodeValue(widget: WidgetLayout) {
|
|
||||||
if (widget) {
|
|
||||||
node = widget.node as ComfyImageCompareNode
|
|
||||||
nodeValue = node.value;
|
|
||||||
attrsChanged = widget.attrsChanged;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const urlPattern = /^((http|https|ftp):\/\/)/;
|
|
||||||
|
|
||||||
$: updateUrls($nodeValue);
|
|
||||||
|
|
||||||
function updateUrls(value: ImageCompareData) {
|
|
||||||
leftUrl = ""
|
|
||||||
rightUrl = ""
|
|
||||||
console.warn("UPD", value)
|
|
||||||
|
|
||||||
if (typeof value[0] === "string") {
|
|
||||||
if (urlPattern.test(value[0]))
|
|
||||||
leftUrl = value[0]
|
|
||||||
else
|
|
||||||
leftUrl = convertFilenameToComfyURL(value[0])
|
|
||||||
}
|
|
||||||
if (typeof value[1] === "string") {
|
|
||||||
if (urlPattern.test(value[1]))
|
|
||||||
rightUrl = value[1]
|
|
||||||
else
|
|
||||||
rightUrl = convertFilenameToComfyURL(value[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="wrapper comfy-compare-widget">
|
|
||||||
<Block>
|
|
||||||
<Tabs elem_classes={["gradio-tabs"]}>
|
|
||||||
<TabItem name="Slider">
|
|
||||||
<ImageComparison>
|
|
||||||
{#if leftUrl && leftUrl != ""}
|
|
||||||
{@const props = { slot: "first" }}
|
|
||||||
<img {...props} alt="Left" src={leftUrl} />
|
|
||||||
{/if}
|
|
||||||
{#if rightUrl && leftUrl != ""}
|
|
||||||
{@const props = { slot: "second" }}
|
|
||||||
<img {...props} alt="Right" src={rightUrl} />
|
|
||||||
{/if}
|
|
||||||
</ImageComparison>
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
</Block>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.comfy-compare-widget {
|
|
||||||
max-width: 40rem;
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
343
src/lib/widgets/ImageEditorWidget.svelte
Normal file
343
src/lib/widgets/ImageEditorWidget.svelte
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
|
import { Block } from "@gradio/atoms";
|
||||||
|
import { TextBox } from "@gradio/form";
|
||||||
|
import Row from "$lib/components/gradio/app/Row.svelte";
|
||||||
|
import { get, type Writable } from "svelte/store";
|
||||||
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
|
import { Button } from "@gradio/button";
|
||||||
|
import type { ComfyImageEditorNode, ComfyImageLocation } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
|
import { Embed as Klecks } from "klecks";
|
||||||
|
|
||||||
|
import "klecks/style/style.scss";
|
||||||
|
import ImageUpload from "$lib/components/ImageUpload.svelte";
|
||||||
|
import { uploadImageToComfyUI, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile, type ComfyUploadImageType } from "$lib/utils";
|
||||||
|
import notify from "$lib/notify";
|
||||||
|
import NumberInput from "$lib/components/NumberInput.svelte";
|
||||||
|
|
||||||
|
export let widget: WidgetLayout | null = null;
|
||||||
|
export let isMobile: boolean = false;
|
||||||
|
let node: ComfyImageEditorNode | null = null;
|
||||||
|
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||||
|
let attrsChanged: Writable<number> | null = null;
|
||||||
|
|
||||||
|
let imgWidth: number = 0;
|
||||||
|
let imgHeight: number = 0;
|
||||||
|
|
||||||
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
|
$: if ($nodeValue && $nodeValue.length > 0) {
|
||||||
|
// TODO improve
|
||||||
|
if (imgWidth > 0 && imgHeight > 0) {
|
||||||
|
$nodeValue[0].width = imgWidth
|
||||||
|
$nodeValue[0].height = imgHeight
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$nodeValue[0].width = 0
|
||||||
|
$nodeValue[0].height = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
|
if (widget) {
|
||||||
|
node = widget.node as ComfyImageEditorNode
|
||||||
|
nodeValue = node.value;
|
||||||
|
attrsChanged = widget.attrsChanged;
|
||||||
|
status = $nodeValue && $nodeValue.length > 0 ? "uploaded" : "empty"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let editorRoot: HTMLDivElement | null = null;
|
||||||
|
let showModal = false;
|
||||||
|
let kl: Klecks | null = null;
|
||||||
|
|
||||||
|
function disposeEditor() {
|
||||||
|
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
|
||||||
|
|
||||||
|
if (editorRoot) {
|
||||||
|
while (editorRoot.firstChild) {
|
||||||
|
editorRoot.removeChild(editorRoot.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kl = null;
|
||||||
|
showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBlankCanvas(width: number, height: number, fill: string = "#fff"): HTMLCanvasElement {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = fill,
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.restore();
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadImage(imageURL: string): Promise<HTMLImageElement> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const e = new Image();
|
||||||
|
e.setAttribute('crossorigin', 'anonymous'); // Don't taint the canvas from loading files on-disk
|
||||||
|
e.addEventListener("load", () => { resolve(e); });
|
||||||
|
e.src = imageURL;
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateImageCanvas(imageURL: string): Promise<[HTMLCanvasElement, number, number]> {
|
||||||
|
const image = await loadImage(imageURL);
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = "rgba(255, 255, 255, 0.0)";
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
ctx.restore();
|
||||||
|
return [canvas, image.width, image.height];
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILENAME: string = "ComfyUITemp.png";
|
||||||
|
const SUBFOLDER: string = "ComfyBox_Editor";
|
||||||
|
const DIRECTORY: ComfyUploadImageType = "input";
|
||||||
|
|
||||||
|
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
|
||||||
|
const blob = kl.getPNG();
|
||||||
|
|
||||||
|
status = "uploading"
|
||||||
|
|
||||||
|
await uploadImageToComfyUI(blob, FILENAME, DIRECTORY, SUBFOLDER)
|
||||||
|
.then((entry: ComfyImageLocation) => {
|
||||||
|
const meta: ComfyBoxImageMetadata = comfyFileToComfyBoxMetadata(entry);
|
||||||
|
$nodeValue = [meta] // TODO more than one image
|
||||||
|
status = "uploaded"
|
||||||
|
notify("Saved image to ComfyUI!", { type: "success" })
|
||||||
|
onSuccess();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notify(`Failed to upload image from editor: ${err}`, { type: "error", timeout: 10000 })
|
||||||
|
status = "error"
|
||||||
|
uploadError = err;
|
||||||
|
$nodeValue = []
|
||||||
|
onError();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let closeDialog = null;
|
||||||
|
|
||||||
|
async function saveAndClose() {
|
||||||
|
console.log(closeDialog, kl)
|
||||||
|
if (!closeDialog || !kl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
submitKlecksToComfyUI(() => {}, () => {});
|
||||||
|
closeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
let blankImageWidth = 512;
|
||||||
|
let blankImageHeight = 512;
|
||||||
|
|
||||||
|
async function openImageEditor() {
|
||||||
|
if (!editorRoot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
showModal = true;
|
||||||
|
|
||||||
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
|
||||||
|
kl = new Klecks({
|
||||||
|
embedUrl: url,
|
||||||
|
onSubmit: submitKlecksToComfyUI,
|
||||||
|
targetEl: editorRoot,
|
||||||
|
warnOnPageClose: false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.warn("[ImageEditorWidget] OPENING", widget, $nodeValue)
|
||||||
|
|
||||||
|
let canvas = null;
|
||||||
|
let width = blankImageWidth;
|
||||||
|
let height = blankImageHeight;
|
||||||
|
|
||||||
|
if ($nodeValue && $nodeValue.length > 0) {
|
||||||
|
const comfyImage = $nodeValue[0];
|
||||||
|
const comfyURL = comfyBoxImageToComfyURL(comfyImage);
|
||||||
|
[canvas, width, height] = await generateImageCanvas(comfyURL);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
canvas = generateBlankCanvas(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
kl.openProject({
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
layers: [{
|
||||||
|
name: 'Image',
|
||||||
|
opacity: 1,
|
||||||
|
mixModeStr: 'source-over',
|
||||||
|
image: canvas
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
kl.klApp?.out("yo");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = "empty";
|
||||||
|
let uploadError = null;
|
||||||
|
|
||||||
|
function onUploading() {
|
||||||
|
console.warn("UPLOADING!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "uploading"
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
|
||||||
|
console.warn("UPLOADED!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "uploaded"
|
||||||
|
$nodeValue = e.detail.map(comfyFileToComfyBoxMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
console.warn("CLEAR!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "empty"
|
||||||
|
$nodeValue = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploadError(e: CustomEvent<any>) {
|
||||||
|
console.warn("ERROR!!!")
|
||||||
|
status = "error"
|
||||||
|
uploadError = e.detail
|
||||||
|
$nodeValue = []
|
||||||
|
notify(`Failed to upload image to ComfyUI: ${uploadError}`, { type: "error", timeout: 10000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
let _value: ComfyImageLocation[] = []
|
||||||
|
$: if ($nodeValue)
|
||||||
|
_value = $nodeValue.map(comfyBoxImageToComfyFile)
|
||||||
|
else
|
||||||
|
_value = []
|
||||||
|
|
||||||
|
$: canEdit = status === "empty" || status === "uploaded";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper comfy-image-editor">
|
||||||
|
{#if widget.attrs.variant === "fileUpload" || isMobile}
|
||||||
|
<ImageUpload value={_value}
|
||||||
|
bind:imgWidth
|
||||||
|
bind:imgHeight
|
||||||
|
fileCount={"single"}
|
||||||
|
elem_classes={[]}
|
||||||
|
style={""}
|
||||||
|
label={widget.attrs.title}
|
||||||
|
on:uploading={onUploading}
|
||||||
|
on:uploaded={onUploaded}
|
||||||
|
on:upload_error={onUploadError}
|
||||||
|
on:clear={onClear}
|
||||||
|
on:change={onChange}
|
||||||
|
on:image_clicked={openImageEditor}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="comfy-image-editor-panel">
|
||||||
|
<ImageUpload value={_value}
|
||||||
|
bind:imgWidth
|
||||||
|
bind:imgHeight
|
||||||
|
fileCount={"single"}
|
||||||
|
elem_classes={[]}
|
||||||
|
style={""}
|
||||||
|
label={widget.attrs.title}
|
||||||
|
on:uploading={onUploading}
|
||||||
|
on:uploaded={onUploaded}
|
||||||
|
on:upload_error={onUploadError}
|
||||||
|
on:clear={onClear}
|
||||||
|
on:change={onChange}
|
||||||
|
on:image_clicked={openImageEditor}
|
||||||
|
/>
|
||||||
|
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog>
|
||||||
|
<div>
|
||||||
|
<div id="klecks-loading-screen">
|
||||||
|
<span id="klecks-loading-screen-text"></span>
|
||||||
|
</div>
|
||||||
|
<div class="image-editor-root" bind:this={editorRoot} />
|
||||||
|
</div>
|
||||||
|
<div slot="buttons">
|
||||||
|
<Button variant="primary" on:click={saveAndClose}>
|
||||||
|
Save and Close
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" on:click={closeDialog}>
|
||||||
|
Discard Edits
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<Block>
|
||||||
|
{#if !$nodeValue || $nodeValue.length === 0}
|
||||||
|
<Row>
|
||||||
|
<Row>
|
||||||
|
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||||
|
Create Image
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
||||||
|
</div>
|
||||||
|
{#if uploadError}
|
||||||
|
<div>
|
||||||
|
Upload error: {uploadError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<NumberInput label={"Width"} min={64} max={2048} step={64} bind:value={blankImageWidth} />
|
||||||
|
<NumberInput label={"Height"} min={64} max={2048} step={64} bind:value={blankImageHeight} />
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
{:else}
|
||||||
|
<Row>
|
||||||
|
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||||
|
Edit Image
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
||||||
|
</div>
|
||||||
|
{#if uploadError}
|
||||||
|
<div>
|
||||||
|
Upload error: {uploadError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Row>
|
||||||
|
{/if}
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-editor-root {
|
||||||
|
width: 75vw;
|
||||||
|
height: 75vh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
:global(> .g-root) {
|
||||||
|
height: calc(100% - 59px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-image-editor {
|
||||||
|
:global(> dialog) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.kl-popup) {
|
||||||
|
z-index: 999999999999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Block, BlockLabel, Empty } from "@gradio/atoms";
|
|
||||||
import { File as FileIcon } from "@gradio/icons";
|
|
||||||
import { ModifyUpload, Upload, blobToBase64, normalise_file } from "@gradio/upload";
|
|
||||||
import type { WidgetLayout } from "$lib/stores/layoutState";
|
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import type { ComfyGalleryNode, ComfyImageUploadNode, GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
|
||||||
import UploadText from "$lib/components/gradio/app/UploadText.svelte";
|
|
||||||
import { tick } from "svelte";
|
|
||||||
import notify from "$lib/notify";
|
|
||||||
import type { Vector2 } from "@litegraph-ts/core";
|
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
|
||||||
export let isMobile: boolean = false;
|
|
||||||
let node: ComfyImageUploadNode | null = null;
|
|
||||||
let nodeValue: Writable<Array<GradioFileData>> | null = null;
|
|
||||||
let propsChanged: Writable<number> | null = null;
|
|
||||||
let dragging = false;
|
|
||||||
let pending_upload = false;
|
|
||||||
let old_value: Array<GradioFileData> | null = null;
|
|
||||||
let imgElem: HTMLImageElement | null = null
|
|
||||||
let imgWidth: number = 1;
|
|
||||||
let imgHeight: number = 1;
|
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
|
||||||
|
|
||||||
let _value: GradioFileData[] | null = null;
|
|
||||||
const root = "comf"
|
|
||||||
const root_url = "https//ComfyUI!"
|
|
||||||
|
|
||||||
$: _value = normalise_file($nodeValue, root, root_url);
|
|
||||||
|
|
||||||
function setNodeValue(widget: WidgetLayout) {
|
|
||||||
if (widget) {
|
|
||||||
node = widget.node as ComfyImageUploadNode
|
|
||||||
nodeValue = node.value;
|
|
||||||
propsChanged = node.propsChanged;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$: if (!(node && _value && _value.length > 0 && imgElem)) {
|
|
||||||
imgWidth = 1
|
|
||||||
imgHeight = 1
|
|
||||||
node.imageSize = [1, 1]
|
|
||||||
}
|
|
||||||
else if (imgWidth > 1 || imgHeight > 1) {
|
|
||||||
node.imageSize = [imgWidth, imgHeight]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
node.imageSize = [1, 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChange() {
|
|
||||||
$nodeValue = _value || []
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUpload() {
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClear() {
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GradioUploadResponse {
|
|
||||||
error?: string;
|
|
||||||
files?: Array<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
|
|
||||||
console.debug("UPLOADILFES", root, files);
|
|
||||||
|
|
||||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
|
||||||
|
|
||||||
const requests = files.map(async (file) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("image", file, file.name);
|
|
||||||
return fetch(new Request(url + "/upload/image", {
|
|
||||||
body: formData,
|
|
||||||
method: 'POST'
|
|
||||||
}))
|
|
||||||
.then(r => r.json())
|
|
||||||
.catch(error => error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(requests)
|
|
||||||
.then( (results) => {
|
|
||||||
const errors = []
|
|
||||||
const files = []
|
|
||||||
|
|
||||||
for (const r of results) {
|
|
||||||
if (r instanceof Error) {
|
|
||||||
errors.push(r.cause)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// bare filename of image
|
|
||||||
files.push((r as ComfyUploadImageAPIResponse).name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let error = null;
|
|
||||||
if (errors && errors.length > 0)
|
|
||||||
error = "Upload error(s):\n" + errors.join("\n");
|
|
||||||
|
|
||||||
return { error, files }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (JSON.stringify(_value) !== JSON.stringify(old_value)) {
|
|
||||||
pending_upload = true;
|
|
||||||
|
|
||||||
old_value = _value;
|
|
||||||
|
|
||||||
if (_value == null)
|
|
||||||
_value = []
|
|
||||||
else if (!Array.isArray(_value))
|
|
||||||
_value = [_value]
|
|
||||||
|
|
||||||
const allBlobs = _value.map((file_data: GradioFileData) => file_data.blob)
|
|
||||||
|
|
||||||
if (allBlobs == null || allBlobs.length === 0) {
|
|
||||||
_value = null;
|
|
||||||
onChange();
|
|
||||||
pending_upload = false;
|
|
||||||
}
|
|
||||||
else if (!allBlobs.every(b => b != null)) {
|
|
||||||
_value = null;
|
|
||||||
pending_upload = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let files = (Array.isArray(_value) ? _value : [_value]).map(
|
|
||||||
(file_data) => file_data.blob!
|
|
||||||
);
|
|
||||||
let upload_value = _value;
|
|
||||||
pending_upload = true;
|
|
||||||
upload_files(root, files).then((response) => {
|
|
||||||
if (JSON.stringify(upload_value) !== JSON.stringify(_value)) {
|
|
||||||
// value has changed since upload started
|
|
||||||
console.error("[ImageUploadWidget] value has changed since upload started", upload_value, _value)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending_upload = false;
|
|
||||||
_value.forEach(
|
|
||||||
(file_data: GradioFileData, i: number) => {
|
|
||||||
if (response.files) {
|
|
||||||
file_data.orig_name = file_data.name;
|
|
||||||
file_data.name = response.files[i];
|
|
||||||
file_data.is_file = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.error) {
|
|
||||||
notify(response.error, { type: "error" })
|
|
||||||
}
|
|
||||||
|
|
||||||
$nodeValue = normalise_file(_value, root, root_url) as GradioFileData[];
|
|
||||||
onChange();
|
|
||||||
onUpload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handle_upload({ detail }: CustomEvent<GradioFileData | Array<GradioFileData>>) {
|
|
||||||
_value = Array.isArray(detail) ? detail : [detail];
|
|
||||||
await tick();
|
|
||||||
onChange();
|
|
||||||
onUpload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_clear(_e: CustomEvent<null>) {
|
|
||||||
_value = null;
|
|
||||||
onChange();
|
|
||||||
onClear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImageUrl(image: GradioFileData) {
|
|
||||||
const baseUrl = `http://${location.hostname}:8188` // TODO make configurable
|
|
||||||
const params = new URLSearchParams({ filename: image.name, subfolder: "", type: "input" })
|
|
||||||
return `${baseUrl}/view?${params}`
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="wrapper gradio-file comfy-image-upload" style={widget.attrs.style}>
|
|
||||||
{#if widget && node && nodeValue}
|
|
||||||
<Block
|
|
||||||
visible={true}
|
|
||||||
variant={($nodeValue === null || $nodeValue.length === 0) ? "dashed" : "solid"}
|
|
||||||
border_mode={dragging ? "focus" : "base"}
|
|
||||||
padding={true}
|
|
||||||
elem_id="comfy-image-upload-block"
|
|
||||||
elem_classes={widget.attrs.classes.split(",")}
|
|
||||||
>
|
|
||||||
<BlockLabel
|
|
||||||
label={widget.attrs.title}
|
|
||||||
show_label={widget.attrs.title != ""}
|
|
||||||
Icon={FileIcon}
|
|
||||||
float={widget.attrs.title != ""}
|
|
||||||
/>
|
|
||||||
{#if _value && _value.length > 0 && !pending_upload}
|
|
||||||
{@const firstImage = _value[0]}
|
|
||||||
<ModifyUpload on:clear={handle_clear} absolute />
|
|
||||||
<img src={getImageUrl(firstImage)}
|
|
||||||
alt={firstImage.orig_name}
|
|
||||||
bind:this={imgElem}
|
|
||||||
bind:naturalWidth={imgWidth}
|
|
||||||
bind:naturalHeight={imgHeight}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Upload
|
|
||||||
file_count={node.properties.fileCount}
|
|
||||||
filetype="image/*"
|
|
||||||
on:change={({ detail }) => ($nodeValue = detail)}
|
|
||||||
on:load={handle_upload}
|
|
||||||
bind:dragging
|
|
||||||
on:clear
|
|
||||||
on:select
|
|
||||||
parse_to_data_url={false}
|
|
||||||
>
|
|
||||||
<UploadText type="file" />
|
|
||||||
</Upload>
|
|
||||||
{/if}
|
|
||||||
</Block>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.comfy-image-upload {
|
|
||||||
height: var(--size-96);
|
|
||||||
|
|
||||||
:global(.block) {
|
|
||||||
height: inherit;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { LiteGraph } from '@litegraph-ts/core';
|
||||||
import App from './App.svelte';
|
import App from './App.svelte';
|
||||||
|
|
||||||
|
LiteGraph.use_uuids = true;
|
||||||
|
|
||||||
const app = new App({
|
const app = new App({
|
||||||
target: document.getElementById('app'),
|
target: document.getElementById('app'),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import ComfyGraph from '$lib/ComfyGraph';
|
|||||||
|
|
||||||
Framework7.use(Framework7Svelte);
|
Framework7.use(Framework7Svelte);
|
||||||
|
|
||||||
|
LiteGraph.use_uuids = true;
|
||||||
LiteGraph.dialog_close_on_mouse_leave = false;
|
LiteGraph.dialog_close_on_mouse_leave = false;
|
||||||
LiteGraph.search_hide_on_mouse_leave = false;
|
LiteGraph.search_hide_on_mouse_leave = false;
|
||||||
LiteGraph.pointerevents_method = "pointer";
|
LiteGraph.pointerevents_method = "pointer";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import tsconfigPaths from 'vite-tsconfig-paths';
|
|||||||
import FullReload from 'vite-plugin-full-reload';
|
import FullReload from 'vite-plugin-full-reload';
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||||
import removeConsole from 'vite-plugin-svelte-console-remover';
|
import removeConsole from 'vite-plugin-svelte-console-remover';
|
||||||
|
import glsl from 'vite-plugin-glsl';
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
console.log("Production build: " + isProduction)
|
console.log("Production build: " + isProduction)
|
||||||
@@ -20,6 +21,7 @@ export default defineConfig({
|
|||||||
// "src/**/ComfyApp.{ts,svelte}"
|
// "src/**/ComfyApp.{ts,svelte}"
|
||||||
// ]),
|
// ]),
|
||||||
isProduction && removeConsole(),
|
isProduction && removeConsole(),
|
||||||
|
glsl(),
|
||||||
svelte(),
|
svelte(),
|
||||||
viteStaticCopy({
|
viteStaticCopy({
|
||||||
targets: [
|
targets: [
|
||||||
|
|||||||
Reference in New Issue
Block a user