From affe97337546a065c053b324203b774ff75dba49 Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 10:02:52 -0500
Subject: [PATCH 01/15] Clear all subgraphs when closing
---
src/lib/stores/workflowState.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/lib/stores/workflowState.ts b/src/lib/stores/workflowState.ts
index b119593..9efca6f 100644
--- a/src/lib/stores/workflowState.ts
+++ b/src/lib/stores/workflowState.ts
@@ -134,6 +134,7 @@ export class ComfyWorkflow {
return;
}
+ canvas.canvas.closeAllSubgraphs();
this.graph.detachCanvas(canvas.canvas);
this.graph.eventBus.removeListener("afterExecute", canvas.canvasHandler)
From c3ab3aa69afac96e89f23f16e672605cd133a368 Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 11:10:10 -0500
Subject: [PATCH 02/15] Start vanilla workflow conversion, better PNG parser
based on catbox userscript code
---
src/lib/api.ts | 12 +-
src/lib/components/ComfyApp.ts | 54 ++++--
src/lib/components/ComfyQueue.svelte | 7 +-
src/lib/components/ComfyWorkflowsView.svelte | 4 +-
src/lib/components/PromptDisplay.svelte | 7 +-
src/lib/convertVanillaWorkflow.ts | 51 ++++++
src/lib/pnginfo.ts | 183 ++++++++++++++-----
src/lib/stores/layoutStates.ts | 22 ++-
tsconfig.json | 1 +
9 files changed, 267 insertions(+), 74 deletions(-)
create mode 100644 src/lib/convertVanillaWorkflow.ts
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 3d0a960..3c1f413 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -1,4 +1,4 @@
-import type { Progress, SerializedPrompt, SerializedPromptInputsForNode, SerializedPromptInputsAll, SerializedPromptOutputs } from "./components/ComfyApp";
+import type { Progress, SerializedPrompt, SerializedPromptInputsForNode, SerializedPromptInputsAll, SerializedPromptOutputs, SerializedAppState } from "./components/ComfyApp";
import type TypedEmitter from "typed-emitter";
import EventEmitter from "events";
import type { ComfyImageLocation } from "$lib/utils";
@@ -57,10 +57,14 @@ export type ComfyAPIHistoryResponse = {
error?: string
}
+export type SerializedComfyBoxPromptData = {
+ subgraphs: string[]
+}
+
export type ComfyPromptPNGInfo = {
- workflow: SerializedLGraph,
- comfyBoxLayout: SerializedLayoutState,
- comfyBoxSubgraphs: string[],
+ workflow?: SerializedLGraph, // ComfyUI format
+ comfyBoxWorkflow: SerializedAppState,
+ comfyBoxPrompt: SerializedComfyBoxPromptData,
}
export type ComfyBoxPromptExtraData = ComfyUIPromptExtraData & {
diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts
index a713d31..173cfe4 100644
--- a/src/lib/components/ComfyApp.ts
+++ b/src/lib/components/ComfyApp.ts
@@ -1,7 +1,7 @@
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex, type UUID } from "@litegraph-ts/core";
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
-import { getPngMetadata, importA1111 } from "$lib/pnginfo";
+import { getPngMetadata, importA1111, parsePNGMetadata } from "$lib/pnginfo";
import EventEmitter from "events";
import type TypedEmitter from "typed-emitter";
@@ -44,6 +44,7 @@ import selectionState from "$lib/stores/selectionState";
import layoutStates from "$lib/stores/layoutStates";
import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
import workflowState from "$lib/stores/workflowState";
+import convertVanillaWorkflow from "$lib/convertVanillaWorkflow";
export const COMFYBOX_SERIAL_VERSION = 1;
@@ -72,8 +73,10 @@ export type A1111PromptAndInfo = {
* Represents a single workflow that can be loaded into the program from JSON.
*/
export type SerializedAppState = {
- /** Program identifier, should always be "ComfyBox" */
- createdBy: "ComfyBox",
+ /** For easy structural typing use */
+ comfyBoxWorkflow: true,
+ /** Program identifier, should be something like "ComfyBox" or "ComfyUI" */
+ createdBy: string,
/** Serial version, should be incremented on breaking changes */
version: number,
/** Commit hash if found */
@@ -138,6 +141,14 @@ type CanvasState = {
canvas: ComfyGraphCanvas,
}
+function isComfyBoxWorkflow(data: any): data is SerializedAppState {
+ return data != null && (typeof data === "object") && data.comfyBoxWorkflow;
+}
+
+function isVanillaWorkflow(data: any): data is SerializedLGraph {
+ return data != null && (typeof data === "object") && data.last_node_id != null;
+}
+
export default class ComfyApp {
api: ComfyAPI;
@@ -237,6 +248,7 @@ export default class ComfyApp {
const canvas = this.lCanvas.serialize();
return {
+ comfyBoxWorkflow: true,
createdBy: "ComfyBox",
version: COMFYBOX_SERIAL_VERSION,
commitHash: __GIT_COMMIT_HASH__,
@@ -399,7 +411,7 @@ export default class ComfyApp {
} catch (error) { }
}
- if (workflow && workflow.createdBy === "ComfyBox") {
+ if (workflow && typeof workflow.createdBy === "string") {
this.openWorkflow(workflow);
}
else {
@@ -537,6 +549,13 @@ export default class ComfyApp {
return workflow;
}
+ async openVanillaWorkflow(data: SerializedLGraph) {
+ const converted = convertVanillaWorkflow(data)
+ console.info("WORKFLWO", converted)
+ notify("Converted ComfyUI workflow to ComfyBox format.", { type: "info" })
+ // await this.openWorkflow(JSON.parse(pngInfo.workflow));
+ }
+
setActiveWorkflow(id: WorkflowInstID) {
const index = get(workflowState).openedWorkflows.findIndex(w => w.id === id)
if (index === -1)
@@ -695,7 +714,7 @@ export default class ComfyApp {
}
const p = this.graphToPrompt(workflow, tag);
- const l = workflow.layout.serialize();
+ const wf = this.serialize(workflow)
console.debug(graphToGraphVis(workflow.graph))
console.debug(promptToGraphVis(p))
@@ -704,9 +723,10 @@ export default class ComfyApp {
const extraData: ComfyBoxPromptExtraData = {
extra_pnginfo: {
- workflow: p.workflow,
- comfyBoxLayout: l,
- comfyBoxSubgraphs: [tag],
+ comfyBoxWorkflow: wf,
+ comfyBoxPrompt: {
+ subgraphs: [tag]
+ }
},
thumbnails
}
@@ -761,10 +781,14 @@ export default class ComfyApp {
*/
async handleFile(file: File) {
if (file.type === "image/png") {
- const pngInfo = await getPngMetadata(file);
+ const buffer = await file.arrayBuffer();
+ const pngInfo = await parsePNGMetadata(buffer);
if (pngInfo) {
- if (pngInfo.comfyBoxConfig) {
- await this.openWorkflow(JSON.parse(pngInfo.comfyBoxConfig));
+ if (pngInfo.comfyBoxWorkflow) {
+ await this.openWorkflow(JSON.parse(pngInfo.comfyBoxWorkflow));
+ } else if (pngInfo.workflow) {
+ const workflow = JSON.parse(pngInfo.workflow);
+ await this.openVanillaWorkflow(workflow);
} else if (pngInfo.parameters) {
const parsed = parseA1111(pngInfo.parameters)
if ("error" in parsed) {
@@ -787,7 +811,13 @@ export default class ComfyApp {
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
const reader = new FileReader();
reader.onload = async () => {
- await this.openWorkflow(JSON.parse(reader.result as string));
+ const result = JSON.parse(reader.result as string)
+ if (isComfyBoxWorkflow(result)) {
+ await this.openWorkflow(result);
+ }
+ else if (isVanillaWorkflow(result)) {
+ await this.openVanillaWorkflow(result);
+ }
};
reader.readAsText(file);
}
diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte
index bdf0b2b..60315c9 100644
--- a/src/lib/components/ComfyQueue.svelte
+++ b/src/lib/components/ComfyQueue.svelte
@@ -69,7 +69,7 @@
dateStr = formatDate(date);
}
- const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxSubgraphs;
+ const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs;
let message = "Prompt";
if (entry.workflowID != null) {
@@ -208,7 +208,7 @@
-
+
{#if _entries.length > 0}
{#each _entries as entry}
@@ -305,8 +305,9 @@
diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte
index 96cab3f..47f5012 100644
--- a/src/lib/components/Modal.svelte
+++ b/src/lib/components/Modal.svelte
@@ -50,7 +50,7 @@
-
diff --git a/src/lib/components/A1111PromptDisplay.svelte b/src/lib/components/modal/A1111PromptModal.svelte
similarity index 93%
rename from src/lib/components/A1111PromptDisplay.svelte
rename to src/lib/components/modal/A1111PromptModal.svelte
index 134ff5b..6716a77 100644
--- a/src/lib/components/A1111PromptDisplay.svelte
+++ b/src/lib/components/modal/A1111PromptModal.svelte
@@ -1,11 +1,10 @@
+
+
+ When loading the graph, the following node types were not found:
+
+ {#each Array.from(missingNodeTypes) as type}
+ - {type}
+ {/each}
+
+ Do you want to convert the workflow into ComfyBox format anyway? (You may lose some workflow state)
+
+
+
diff --git a/src/lib/components/modal/MissingNodeTypesModal.svelte b/src/lib/components/modal/MissingNodeTypesModal.svelte
new file mode 100644
index 0000000..53f8326
--- /dev/null
+++ b/src/lib/components/modal/MissingNodeTypesModal.svelte
@@ -0,0 +1,26 @@
+
+
+
+ When loading the graph, the following node types were not found:
+
+ {#each Array.from(missingNodeTypes) as type}
+ - {type}
+ {/each}
+
+ Nodes that have failed to load will show as red on the graph.
+
+
+
diff --git a/src/lib/components/modal/WorkflowLoadErrorModal.svelte b/src/lib/components/modal/WorkflowLoadErrorModal.svelte
new file mode 100644
index 0000000..812735d
--- /dev/null
+++ b/src/lib/components/modal/WorkflowLoadErrorModal.svelte
@@ -0,0 +1,38 @@
+
+
+
+ Loading aborted due to error reloading workflow data:
+
+
+ {error.toString()}
+
+
+ {error?.stack || "(No stacktrace available)"}
+
+
+
+
+
diff --git a/src/lib/convertVanillaWorkflow.ts b/src/lib/convertVanillaWorkflow.ts
index 50933f0..ddd8075 100644
--- a/src/lib/convertVanillaWorkflow.ts
+++ b/src/lib/convertVanillaWorkflow.ts
@@ -1,6 +1,6 @@
import { LGraph, type INodeInputSlot, type SerializedLGraph, type LinkID, type UUID, type NodeID, LiteGraph, BuiltInSlotType, type SerializedLGraphNode, type Vector2, BuiltInSlotShape, type INodeOutputSlot } from "@litegraph-ts/core";
import type { SerializedAppState } from "./components/ComfyApp";
-import layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState } from "./stores/layoutStates";
+import layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState, type WritableLayoutStateStore } from "./stores/layoutStates";
import { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState";
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
import ComfyApp from "./components/ComfyApp";
@@ -158,7 +158,7 @@ function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) {
}
}
-export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): ComfyWorkflow {
+export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): [ComfyWorkflow, WritableLayoutStateStore] {
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
const { root, left, right } = layoutState.initDefaultLayout();
@@ -335,11 +335,5 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
const layout = layoutState.serialize();
comfyBoxWorkflow.deserialize(layoutState, { graph: vanillaWorkflow, attrs, layout })
- for (const node of comfyBoxWorkflow.graph.iterateNodesInOrder()) {
- if ((node as any).isBackendNode) {
- console.warn("BENDNODE", node)
- }
- }
-
- return comfyBoxWorkflow
+ return [comfyBoxWorkflow, layoutState]
}
diff --git a/src/lib/stores/modalState.ts b/src/lib/stores/modalState.ts
index cf4ab6c..bb22c7e 100644
--- a/src/lib/stores/modalState.ts
+++ b/src/lib/stores/modalState.ts
@@ -5,16 +5,18 @@ import { v4 as uuidv4 } from "uuid";
export type ModalButton = {
name: string,
variant: "primary" | "secondary",
- onClick: () => void
+ onClick: () => void,
+ closeOnClick?: boolean
}
export interface ModalData {
id: string,
- title: string,
+ title?: string,
onClose?: () => void,
svelteComponent?: typeof SvelteComponentDev,
- svelteProps?: Record
,
- buttons?: ModalButton[],
- showCloseButton?: boolean
+ svelteProps: Record,
+ buttons: ModalButton[],
+ showCloseButton: boolean,
+ closeOnClick: boolean
}
export interface ModalState {
activeModals: ModalData[]
@@ -34,7 +36,10 @@ const store: Writable = writable(
function pushModal(data: Partial) {
const modal: ModalData = {
- title: "Modal",
+ showCloseButton: true,
+ closeOnClick: true,
+ buttons: [],
+ svelteProps: {},
...data,
id: uuidv4(),
}
diff --git a/src/lib/stores/workflowState.ts b/src/lib/stores/workflowState.ts
index 67ac863..b86d323 100644
--- a/src/lib/stores/workflowState.ts
+++ b/src/lib/stores/workflowState.ts
@@ -79,7 +79,7 @@ export class ComfyWorkflow {
/*
* Missing node types encountered when deserializing the graph
*/
- missingNodeTypes: string[];
+ missingNodeTypes: Set = new Set();
get layout(): WritableLayoutStateStore | null {
return layoutStates.getLayout(this.id)
@@ -168,6 +168,13 @@ export class ComfyWorkflow {
}
}
+ /*
+ * Creates a workflow and layout.
+ *
+ * NOTE: The layout will be attached to the global store, but the workflow
+ * will not. If you change your mind later be sure to call
+ * layoutStates.remove(workflow.id)!
+ */
static create(title: string = "New Workflow"): [ComfyWorkflow, WritableLayoutStateStore] {
const workflow = new ComfyWorkflow(title);
const layoutState = layoutStates.create(workflow);
@@ -175,7 +182,7 @@ export class ComfyWorkflow {
}
deserialize(layoutState: WritableLayoutStateStore, data: SerializedWorkflowState) {
- this.missingNodeTypes = []
+ this.missingNodeTypes.clear();
for (let n of data.graph.nodes) {
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
@@ -183,7 +190,7 @@ export class ComfyWorkflow {
// Find missing node types
if (!(n.type in LiteGraph.registered_node_types)) {
- this.missingNodeTypes.push(n.type);
+ this.missingNodeTypes.add(n.type);
}
}
@@ -233,6 +240,7 @@ type WorkflowStateOps = {
getActiveWorkflow: () => ComfyWorkflow | null
createNewWorkflow: (canvas: ComfyGraphCanvas, title?: string, setActive?: boolean) => ComfyWorkflow,
openWorkflow: (canvas: ComfyGraphCanvas, data: SerializedAppState) => ComfyWorkflow,
+ addWorkflow: (canvas: ComfyGraphCanvas, data: ComfyWorkflow) => void,
closeWorkflow: (canvas: ComfyGraphCanvas, index: number) => void,
closeAllWorkflows: (canvas: ComfyGraphCanvas) => void,
setActiveWorkflow: (canvas: ComfyGraphCanvas, index: number) => ComfyWorkflow | null
@@ -296,6 +304,12 @@ function openWorkflow(canvas: ComfyGraphCanvas, data: SerializedAppState): Comfy
const [workflow, layoutState] = ComfyWorkflow.create("Workflow")
workflow.deserialize(layoutState, { graph: data.workflow, layout: data.layout, attrs: data.attrs })
+ addWorkflow(canvas, workflow);
+
+ return workflow;
+}
+
+function addWorkflow(canvas: ComfyGraphCanvas, workflow: ComfyWorkflow) {
const state = get(store);
state.openedWorkflows.push(workflow);
state.openedWorkflowsByID[workflow.id] = workflow;
@@ -317,7 +331,6 @@ function closeWorkflow(canvas: ComfyGraphCanvas, index: number) {
layoutStates.remove(workflow.id)
-
state.openedWorkflows.splice(index, 1)
delete state.openedWorkflowsByID[workflow.id]
let newIndex = clamp(index, 0, state.openedWorkflows.length - 1)
@@ -372,6 +385,7 @@ const workflowStateStore: WritableWorkflowStateStore =
getActiveWorkflow,
createNewWorkflow,
openWorkflow,
+ addWorkflow,
closeWorkflow,
closeAllWorkflows,
setActiveWorkflow,
diff --git a/src/mobile/GenToolbar.svelte b/src/mobile/GenToolbar.svelte
index eaaa917..a4a13ce 100644
--- a/src/mobile/GenToolbar.svelte
+++ b/src/mobile/GenToolbar.svelte
@@ -41,12 +41,12 @@
return;
navigator.vibrate(20)
+ fileInput.value = null;
fileInput.click();
}
function loadWorkflow(): void {
app.handleFile(fileInput.files[0]);
- fileInput.files = null;
}
function doSaveLocal(): void {
From 9c4e5cea94da0ff792104943a1ffeeee0902fef6 Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 17:35:07 -0500
Subject: [PATCH 07/15] Convert primitive nodes
---
src/lib/components/ComfyWorkflowsView.svelte | 2 +-
src/lib/convertVanillaWorkflow.ts | 224 +++++++++++++++---
.../data/convertedWidgetAndPrimitiveNode.json | 135 +++++++++++
3 files changed, 325 insertions(+), 36 deletions(-)
create mode 100644 src/tests/data/convertedWidgetAndPrimitiveNode.json
diff --git a/src/lib/components/ComfyWorkflowsView.svelte b/src/lib/components/ComfyWorkflowsView.svelte
index 783bacb..ef909d2 100644
--- a/src/lib/components/ComfyWorkflowsView.svelte
+++ b/src/lib/components/ComfyWorkflowsView.svelte
@@ -51,7 +51,7 @@
}
async function doRefreshCombos() {
- await app.refreshComboInNodes(undefined, true)
+ await app.refreshComboInNodes(undefined, undefined, true)
}
function refreshView(event?: Event) {
diff --git a/src/lib/convertVanillaWorkflow.ts b/src/lib/convertVanillaWorkflow.ts
index ddd8075..3388f81 100644
--- a/src/lib/convertVanillaWorkflow.ts
+++ b/src/lib/convertVanillaWorkflow.ts
@@ -1,10 +1,10 @@
-import { LGraph, type INodeInputSlot, type SerializedLGraph, type LinkID, type UUID, type NodeID, LiteGraph, BuiltInSlotType, type SerializedLGraphNode, type Vector2, BuiltInSlotShape, type INodeOutputSlot } from "@litegraph-ts/core";
+import { LGraph, type INodeInputSlot, type SerializedLGraph, type LinkID, type UUID, type NodeID, LiteGraph, BuiltInSlotType, type SerializedLGraphNode, type Vector2, BuiltInSlotShape, type INodeOutputSlot, type SlotType } from "@litegraph-ts/core";
import type { SerializedAppState } from "./components/ComfyApp";
import layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState, type WritableLayoutStateStore } from "./stores/layoutStates";
import { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState";
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
import ComfyApp from "./components/ComfyApp";
-import { iterateNodeDefInputs } from "./ComfyNodeDef";
+import { iterateNodeDefInputs, type ComfyNodeDefInputType, type ComfyNodeDefInputOptions } from "./ComfyNodeDef";
import type { ComfyNodeDefInput } from "./ComfyNodeDef";
import type IComfyInputSlot from "./IComfyInputSlot";
import ComfyWidgets from "./widgets"
@@ -28,10 +28,20 @@ type ComfyUIConvertedWidget = {
config: ComfyNodeDefInput
}
+/*
+ * Input slot for widgets converted to inputs
+ */
interface IComfyUINodeInputSlot extends INodeInputSlot {
widget?: ComfyUIConvertedWidget
}
+/*
+ * Output slot for PrimitiveNode
+ */
+interface IComfyUINodeOutputSlot extends INodeOutputSlot {
+ widget?: ComfyUIConvertedWidget
+}
+
/*
* ComfyUI frontend nodes that should be converted directly to another type.
*/
@@ -158,6 +168,168 @@ function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) {
}
}
+/*
+ * Returns [nodeType, inputType] for a config type, like "FLOAT" -> ["ui/number", "number"]
+ */
+function getWidgetTypesFromConfig(inputType: ComfyNodeDefInputType): [string, SlotType] | null {
+ let widgetNodeType = null;
+ let widgetInputType = null;
+
+ if (Array.isArray(inputType)) {
+ // Combo options of string[]
+ widgetNodeType = "ui/combo";
+ widgetInputType = "string"
+ }
+ else if (inputType in ComfyWidgets) {
+ // Widget type
+ const widgetFactory = ComfyWidgets[inputType]
+ widgetNodeType = widgetFactory.nodeType;
+ widgetInputType = widgetFactory.inputType
+ }
+ else if ("${inputType}:{inputName}" in ComfyWidgets) {
+ // Widget type override for input of type with given name ("seed", "noise_seed")
+ const widgetFactory = ComfyWidgets["${inputType}:{inputName}"]
+ widgetNodeType = widgetFactory.nodeType;
+ widgetInputType = widgetFactory.inputType
+ }
+ else {
+ // Backend type, we can safely ignore this
+ return null;
+ }
+
+ return [widgetNodeType, widgetInputType]
+}
+
+function configureWidgetNodeProperties(serWidgetNode: SerializedComfyWidgetNode, inputOpts?: ComfyNodeDefInputOptions) {
+ inputOpts ||= {}
+ switch (serWidgetNode.type) {
+ case "ui/number":
+ serWidgetNode.properties.min = inputOpts.min || 0;
+ serWidgetNode.properties.max = inputOpts.max || 100;
+ serWidgetNode.properties.step = inputOpts.step || 1;
+ break;
+ case "ui/text":
+ serWidgetNode.properties.multiline = inputOpts.multiline || false;
+ break;
+ }
+}
+
+/*
+ * Attempts to convert a primitive node
+ * The primitive node should be pruned from the graph afterwards
+ */
+function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: SerializedLGraphNode, layoutState: WritableLayoutStateStore, group: ContainerLayout): boolean {
+ // Get the value output
+ // On primitive nodes it's the one in the first slot
+ const mainOutput = (node.outputs || [])[0] as IComfyUINodeOutputSlot;
+ if (!mainOutput || !mainOutput.links) {
+ console.error("PrimitiveNode output had no output with links!", node)
+ return false;
+ }
+
+ const widget = mainOutput.widget;
+ if (widget === null) {
+ console.error("PrimitiveNode output had no widget config!", node)
+ return false;
+ }
+
+ const [widgetType, widgetOpts] = widget.config
+
+ if (!node.widgets_values) {
+ console.error("PrimitiveNode had no serialized widget values!", node)
+ return false;
+ }
+
+ let pair = getWidgetTypesFromConfig(widgetType);
+ if (pair == null) {
+ // This should never happen! Primitive nodes only deal with frontend types!
+ console.error("PrimitiveNode had a backend type configured!", node)
+ return false;
+ }
+
+ let [widgetNodeType, widgetInputType] = pair
+
+ // PrimitiveNode will have a widget in the first slot with the actual value.
+ // The rest are configuration values for e.g. seed action onprompt queue.
+ const value = node.widgets_values[0];
+
+ const [comfyWidgetNode, serWidgetNode] = createSerializedWidgetNode(
+ vanillaWorkflow,
+ node,
+ 0, // first output on the PrimitiveNode
+ false, // this is an output slot index
+ widgetNodeType,
+ value);
+
+ configureWidgetNodeProperties(serWidgetNode, widgetOpts)
+
+ let foundTitle = null;
+ const widgetLayout = layoutState.addWidget(group, comfyWidgetNode)
+ widgetLayout.attrs.title = mainOutput.name;
+
+ // Rewrite links to point to the new widget node
+ const newLinkOutputSlot = serWidgetNode.outputs.findIndex(o => o.name === comfyWidgetNode.outputSlotName)
+ if (newLinkOutputSlot !== -1) {
+ const newLinkOutput = serWidgetNode.outputs[newLinkOutputSlot];
+ // TODO other links need pruning?
+ for (const linkID of mainOutput.links) {
+ const link = vanillaWorkflow.links.find(l => l[0] === linkID)
+ if (link) {
+ link[1] = serWidgetNode.id; // origin node ID
+ link[2] = newLinkOutputSlot; // origin node slot
+ newLinkOutput.links ||= []
+ newLinkOutput.links.push(linkID)
+
+ // Change the title of the widget to the name of the first input connected to
+ if (foundTitle == null) {
+ const targetNode = vanillaWorkflow.nodes.find(n => n.id === link[3]) // target node ID
+ if (targetNode != null) {
+ const foundInput = targetNode.inputs[link[4]] // target node slot
+ if (foundInput != null && foundInput.name) {
+ foundTitle = foundInput.name;
+ widgetLayout.attrs.title = foundTitle;
+ }
+ }
+ }
+ }
+ }
+ // Remove links on the old node so they won't be double-removed when it's pruned
+ mainOutput.links = []
+ }
+ else {
+ console.error("Could not find output slot for new widget node!", comfyWidgetNode, serWidgetNode)
+ }
+
+ return true;
+}
+
+function removeSerializedNode(vanillaWorkflow: SerializedLGraph, node: SerializedLGraphNode) {
+ if (node.outputs) {
+ for (const output of node.outputs) {
+ if (output.links) {
+ vanillaWorkflow.links = vanillaWorkflow.links.filter(l => output.links.indexOf(l[0]) === -1);
+ output.links = []
+ }
+ }
+ }
+ if (node.inputs) {
+ for (const input of node.inputs) {
+ if (input.link) {
+ vanillaWorkflow.links = vanillaWorkflow.links.filter(l => input.link !== l[0]);
+ input.link = null;
+ }
+ }
+ }
+
+ vanillaWorkflow.nodes = vanillaWorkflow.nodes.filter(n => n.id !== node.id);
+}
+
+/*
+ * Converts a workflow saved with vanilla ComfyUI into a ComfyBox workflow,
+ * adding UI nodes for each widget.
+ *
+ * TODO: test this!
+ */
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): [ComfyWorkflow, WritableLayoutStateStore] {
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
const { root, left, right } = layoutState.initDefaultLayout();
@@ -186,9 +358,17 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
// serialized widgets into ComfyWidgetNodes, add new inputs/outputs,
// then attach the new nodes to the slots
+ // Primitive nodes are special since they can interface with converted
+ // widget inputs
+ if (node.type === "PrimitiveNode") {
+ convertPrimitiveNode(vanillaWorkflow, node, layoutState, left)
+ removeSerializedNode(vanillaWorkflow, node);
+ continue
+ }
+
const def = ComfyApp.knownBackendNodes[node.type];
if (def == null) {
- console.error("Unknown backend node", node.type)
+ console.error("[convertVanillaWorkflow] Unknown backend node", node.type)
continue;
}
@@ -213,31 +393,14 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
// This input is a widget, it should be converted to an input
// connected to a ComfyWidgetNode.
- let widgetNodeType = null;
- let widgetInputType = null;
-
- if (Array.isArray(inputType)) {
- // Combo options of string[]
- widgetInputType = "string"
- widgetNodeType = "ui/combo";
- }
- else if (inputType in ComfyWidgets) {
- // Widget type
- const widgetFactory = ComfyWidgets[inputType]
- widgetInputType = widgetFactory.inputType
- widgetNodeType = widgetFactory.nodeType;
- }
- else if ("${inputType}:{inputName}" in ComfyWidgets) {
- // Widget type override for input of type with given name ("seed", "noise_seed")
- const widgetFactory = ComfyWidgets["${inputType}:{inputName}"]
- widgetInputType = widgetFactory.inputType
- widgetNodeType = widgetFactory.nodeType;
- }
- else {
- // Backend type, we can safely ignore this
+ let pair = getWidgetTypesFromConfig(inputType);
+ if (pair == null) {
+ // Input type is backend-only, we can skip adding a UI node here
continue
}
+ let [widgetNodeType, widgetInputType] = pair
+
const newInput: IComfyInputSlot = {
name: inputName,
link: null,
@@ -264,16 +427,7 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
widgetNodeType,
value);
- switch (widgetNodeType) {
- case "ui/number":
- serWidgetNode.properties.min = inputOpts?.min || 0;
- serWidgetNode.properties.max = inputOpts?.max || 100;
- serWidgetNode.properties.step = inputOpts?.step || 1;
- break;
- case "ui/text":
- serWidgetNode.properties.multiline = inputOpts?.multiline || false;
- break;
- }
+ configureWidgetNodeProperties(serWidgetNode, inputOpts)
if (group == null)
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })
diff --git a/src/tests/data/convertedWidgetAndPrimitiveNode.json b/src/tests/data/convertedWidgetAndPrimitiveNode.json
new file mode 100644
index 0000000..6257fed
--- /dev/null
+++ b/src/tests/data/convertedWidgetAndPrimitiveNode.json
@@ -0,0 +1,135 @@
+{
+ "last_node_id": 2,
+ "last_link_id": 1,
+ "nodes": [
+ {
+ "id": 1,
+ "type": "KSampler",
+ "pos": [
+ 843,
+ 567
+ ],
+ "size": [
+ 315,
+ 262
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": null
+ },
+ {
+ "name": "cfg",
+ "type": "FLOAT",
+ "link": 1,
+ "widget": {
+ "name": "cfg",
+ "config": [
+ "FLOAT",
+ {
+ "default": 8,
+ "min": 0,
+ "max": 100
+ }
+ ]
+ },
+ "slot_index": 4
+ }
+ ],
+ "outputs": [
+ {
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "KSampler"
+ },
+ "widgets_values": [
+ 0,
+ "randomize",
+ 20,
+ 8,
+ "euler",
+ "normal",
+ 1
+ ]
+ },
+ {
+ "id": 2,
+ "type": "PrimitiveNode",
+ "pos": [
+ 506,
+ 637
+ ],
+ "size": {
+ "0": 210,
+ "1": 82
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "FLOAT",
+ "type": "FLOAT",
+ "links": [
+ 1
+ ],
+ "slot_index": 0,
+ "widget": {
+ "name": "cfg",
+ "config": [
+ "FLOAT",
+ {
+ "default": 8,
+ "min": 0,
+ "max": 100
+ }
+ ]
+ }
+ }
+ ],
+ "properties": {},
+ "widgets_values": [
+ 8,
+ "fixed"
+ ]
+ }
+ ],
+ "links": [
+ [
+ 1,
+ 2,
+ 0,
+ 1,
+ 4,
+ "FLOAT"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
From c4fe7e0bf1f5bbc89b0504bc2169fe3a9acb4f69 Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 18:00:27 -0500
Subject: [PATCH 08/15] Convert vanilla workflow tests
---
src/lib/components/ComfyApp.ts | 2 --
src/tests/convertVanillaWorkflowTests.ts | 41 ++++++++++++++++++++++++
src/tests/data/objectInfo.json | 1 +
src/tests/testSuite.ts | 1 +
4 files changed, 43 insertions(+), 2 deletions(-)
create mode 100644 src/tests/convertVanillaWorkflowTests.ts
create mode 100644 src/tests/data/objectInfo.json
diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts
index 331e26c..64d0a97 100644
--- a/src/lib/components/ComfyApp.ts
+++ b/src/lib/components/ComfyApp.ts
@@ -166,7 +166,6 @@ function isVanillaWorkflow(data: any): data is SerializedLGraph {
}
type BackendNodeDef = {
- ctor: new (title?: string) => ComfyBackendNode,
nodeDef: ComfyNodeDef
}
@@ -358,7 +357,6 @@ export default class ComfyApp {
LiteGraph.registerNodeType(node);
node.category = nodeDef.category;
ComfyApp.knownBackendNodes[nodeId] = {
- ctor,
nodeDef
}
diff --git a/src/tests/convertVanillaWorkflowTests.ts b/src/tests/convertVanillaWorkflowTests.ts
new file mode 100644
index 0000000..5ef4c1b
--- /dev/null
+++ b/src/tests/convertVanillaWorkflowTests.ts
@@ -0,0 +1,41 @@
+import { expect } from 'vitest';
+import UnitTest from "./UnitTest";
+import { readFile } from "fs/promises"
+import { get } from "svelte/store";
+import convertVanillaWorkflow, { type ComfyVanillaWorkflow } from '$lib/convertVanillaWorkflow';
+import type { WorkflowAttributes } from '$lib/stores/workflowState';
+import { defaultWorkflowAttributes, type IDragItem, type WidgetLayout } from '$lib/stores/layoutStates';
+import ComfyApp from '$lib/components/ComfyApp';
+import { LiteGraph } from '@litegraph-ts/core';
+import type { ComfyNodeDef } from '$lib/ComfyNodeDef';
+
+const objectInfo: Record = await import("./data/objectInfo.json")
+const json1: ComfyVanillaWorkflow = await import("./data/convertedWidgetAndPrimitiveNode.json")
+
+export default class convertVanillaWorkflowTests extends UnitTest {
+ test__convertsPrimitiveNodeAndConvertedInput() {
+ const workflow = LiteGraph.cloneObject(json1)
+ const attrs: WorkflowAttributes = { ...defaultWorkflowAttributes }
+
+ const converted = convertVanillaWorkflow(workflow, attrs)
+
+ ComfyApp.knownBackendNodes["KSampler"] = {
+ nodeDef: objectInfo["KSampler"]
+ }
+
+ expect(converted).toBeInstanceOf(Array)
+
+ const [convWorkflow, convLayout] = converted;
+
+ const layout = get(convLayout)
+
+ expect(Object.keys(layout.allItems)).toHaveLength(4)
+
+ const widget = Object.values(layout.allItems).find(di => di.dragItem.type === "widget")?.dragItem as WidgetLayout;
+ expect(widget).toBeDefined();
+ expect(widget.attrs.title).toEqual("cfg")
+ expect(widget.node).toBeDefined();
+ expect(widget.node.type).toEqual("ui/number")
+ expect(convWorkflow.graph.getNodeById(widget.node.id)).toEqual(widget.node)
+ }
+}
diff --git a/src/tests/data/objectInfo.json b/src/tests/data/objectInfo.json
new file mode 100644
index 0000000..d3888e2
--- /dev/null
+++ b/src/tests/data/objectInfo.json
@@ -0,0 +1 @@
+{"KSampler":{"input":{"required":{"model":["MODEL"],"seed":["INT",{"default":0,"min":0,"max":18446744073709552000}],"steps":["INT",{"default":20,"min":1,"max":10000}],"cfg":["FLOAT",{"default":8,"min":0,"max":100}],"sampler_name":[["euler","euler_ancestral","heun","dpm_2","dpm_2_ancestral","lms","dpm_fast","dpm_adaptive","dpmpp_2s_ancestral","dpmpp_sde","dpmpp_2m","ddim","uni_pc","uni_pc_bh2"]],"scheduler":[["normal","karras","simple","ddim_uniform"]],"positive":["CONDITIONING"],"negative":["CONDITIONING"],"latent_image":["LATENT"],"denoise":["FLOAT",{"default":1,"min":0,"max":1,"step":0.01}]}},"output":["LATENT"],"output_is_list":[false],"output_name":["LATENT"],"name":"KSampler","display_name":"KSampler","description":"","category":"sampling"}}
diff --git a/src/tests/testSuite.ts b/src/tests/testSuite.ts
index 3d067bd..9e96316 100644
--- a/src/tests/testSuite.ts
+++ b/src/tests/testSuite.ts
@@ -2,3 +2,4 @@ export { default as ComfyPromptSerializerTests } from "./ComfyPromptSerializerTe
export { default as ComfyGraphTests } from "./ComfyGraphTests"
export { default as parseA1111Tests } from "./parseA1111Tests"
export { default as convertA1111ToStdPromptTests } from "./convertA1111ToStdPromptTests"
+export { default as convertVanillaWorkflowTest } from "./convertVanillaWorkflowTests"
From 57f635bbfdf12db6d15e0e7bdb8ebd5e10a6ee91 Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 18:07:05 -0500
Subject: [PATCH 09/15] Fix test
---
src/tests/convertVanillaWorkflowTests.ts | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/tests/convertVanillaWorkflowTests.ts b/src/tests/convertVanillaWorkflowTests.ts
index 5ef4c1b..b904de0 100644
--- a/src/tests/convertVanillaWorkflowTests.ts
+++ b/src/tests/convertVanillaWorkflowTests.ts
@@ -4,7 +4,7 @@ import { readFile } from "fs/promises"
import { get } from "svelte/store";
import convertVanillaWorkflow, { type ComfyVanillaWorkflow } from '$lib/convertVanillaWorkflow';
import type { WorkflowAttributes } from '$lib/stores/workflowState';
-import { defaultWorkflowAttributes, type IDragItem, type WidgetLayout } from '$lib/stores/layoutStates';
+import layoutStates, { defaultWorkflowAttributes, type IDragItem, type WidgetLayout } from '$lib/stores/layoutStates';
import ComfyApp from '$lib/components/ComfyApp';
import { LiteGraph } from '@litegraph-ts/core';
import type { ComfyNodeDef } from '$lib/ComfyNodeDef';
@@ -17,23 +17,25 @@ export default class convertVanillaWorkflowTests extends UnitTest {
const workflow = LiteGraph.cloneObject(json1)
const attrs: WorkflowAttributes = { ...defaultWorkflowAttributes }
- const converted = convertVanillaWorkflow(workflow, attrs)
-
ComfyApp.knownBackendNodes["KSampler"] = {
nodeDef: objectInfo["KSampler"]
}
+ const converted = convertVanillaWorkflow(workflow, attrs)
+
expect(converted).toBeInstanceOf(Array)
const [convWorkflow, convLayout] = converted;
const layout = get(convLayout)
- expect(Object.keys(layout.allItems)).toHaveLength(4)
+ expect(Object.keys(layout.allItems)).toHaveLength(10)
- const widget = Object.values(layout.allItems).find(di => di.dragItem.type === "widget")?.dragItem as WidgetLayout;
+ const widgets = Object.values(layout.allItems).filter(di => di.dragItem.type === "widget").map(di => di.dragItem);
+ expect(widgets).toHaveLength(6);
+
+ const widget = widgets.find(w => w.attrs.title === "cfg") as WidgetLayout | null;
expect(widget).toBeDefined();
- expect(widget.attrs.title).toEqual("cfg")
expect(widget.node).toBeDefined();
expect(widget.node.type).toEqual("ui/number")
expect(convWorkflow.graph.getNodeById(widget.node.id)).toEqual(widget.node)
From 93afb64d63ab2c0fb670c1e38d6d5c57e2df3891 Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 18:27:37 -0500
Subject: [PATCH 10/15] Another test
---
src/lib/convertVanillaWorkflow.ts | 64 ++++++++++++++--------
src/lib/nodes/ComfyGraphNode.ts | 2 +-
src/lib/widgets.ts | 33 +++++++++---
src/tests/convertVanillaWorkflowTests.ts | 45 +++++++++++++++-
src/tests/data/convertedWidget.json | 68 ++++++++++++++++++++++++
5 files changed, 178 insertions(+), 34 deletions(-)
create mode 100644 src/tests/data/convertedWidget.json
diff --git a/src/lib/convertVanillaWorkflow.ts b/src/lib/convertVanillaWorkflow.ts
index 3388f81..1fbc021 100644
--- a/src/lib/convertVanillaWorkflow.ts
+++ b/src/lib/convertVanillaWorkflow.ts
@@ -169,41 +169,46 @@ function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) {
}
/*
- * Returns [nodeType, inputType] for a config type, like "FLOAT" -> ["ui/number", "number"]
+ * Returns [nodeType, inputType, addedWidgetCount] for a config type, like "FLOAT" -> ["ui/number", "number", 1]
+ * For "INT:seed" it's ["ui/number", "number", 2] since that type adds a randomizer combo widget
*/
-function getWidgetTypesFromConfig(inputType: ComfyNodeDefInputType): [string, SlotType] | null {
+function getWidgetTypesFromConfig(inputName: string, inputType: ComfyNodeDefInputType): [string, SlotType, number] | null {
let widgetNodeType = null;
let widgetInputType = null;
+ let addedWidgetCount = 1;
if (Array.isArray(inputType)) {
// Combo options of string[]
widgetNodeType = "ui/combo";
widgetInputType = "string"
+ addedWidgetCount = 1;
+ }
+ else if (`${inputType}:${inputName}` in ComfyWidgets) {
+ // Widget type override for input of type with given name ("seed", "noise_seed")
+ const widgetFactory = ComfyWidgets[`${inputType}:${inputName}`]
+ widgetNodeType = widgetFactory.nodeType;
+ widgetInputType = widgetFactory.inputType
+ addedWidgetCount = widgetFactory.addedWidgetCount
}
else if (inputType in ComfyWidgets) {
// Widget type
const widgetFactory = ComfyWidgets[inputType]
widgetNodeType = widgetFactory.nodeType;
widgetInputType = widgetFactory.inputType
- }
- else if ("${inputType}:{inputName}" in ComfyWidgets) {
- // Widget type override for input of type with given name ("seed", "noise_seed")
- const widgetFactory = ComfyWidgets["${inputType}:{inputName}"]
- widgetNodeType = widgetFactory.nodeType;
- widgetInputType = widgetFactory.inputType
+ addedWidgetCount = widgetFactory.addedWidgetCount
}
else {
// Backend type, we can safely ignore this
return null;
}
- return [widgetNodeType, widgetInputType]
+ return [widgetNodeType, widgetInputType, addedWidgetCount]
}
function configureWidgetNodeProperties(serWidgetNode: SerializedComfyWidgetNode, inputOpts?: ComfyNodeDefInputOptions) {
inputOpts ||= {}
switch (serWidgetNode.type) {
- case "ui/number":
+ case `ui/number`:
serWidgetNode.properties.min = inputOpts.min || 0;
serWidgetNode.properties.max = inputOpts.max || 100;
serWidgetNode.properties.step = inputOpts.step || 1;
@@ -240,17 +245,17 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
return false;
}
- let pair = getWidgetTypesFromConfig(widgetType);
+ let pair = getWidgetTypesFromConfig(widget.name, widgetType);
if (pair == null) {
// This should never happen! Primitive nodes only deal with frontend types!
console.error("PrimitiveNode had a backend type configured!", node)
return false;
}
- let [widgetNodeType, widgetInputType] = pair
+ let [widgetNodeType, widgetInputType, addedWidgetCount] = pair
// PrimitiveNode will have a widget in the first slot with the actual value.
- // The rest are configuration values for e.g. seed action onprompt queue.
+ // The rest are configuration values for e.g. seed action on prompt queue.
const value = node.widgets_values[0];
const [comfyWidgetNode, serWidgetNode] = createSerializedWidgetNode(
@@ -384,23 +389,26 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
return i.widget?.name === inputName;
})
+
+ let pair = getWidgetTypesFromConfig(inputName, inputType);
+ if (pair == null) {
+ // Input type is backend-only, we can skip adding a UI node here
+ continue
+ }
+
+ let [widgetNodeType, widgetInputType, widgetCount] = pair
+
if (convertedWidget != null) {
// This input is an extra input slot on the node that should be
// accounted for.
- const [value] = node.widgets_values.splice(0, 1);
+ const values = node.widgets_values.splice(0, widgetCount);
+ const value = values[0]
+ // TODO
}
else {
// This input is a widget, it should be converted to an input
// connected to a ComfyWidgetNode.
- let pair = getWidgetTypesFromConfig(inputType);
- if (pair == null) {
- // Input type is backend-only, we can skip adding a UI node here
- continue
- }
-
- let [widgetNodeType, widgetInputType] = pair
-
const newInput: IComfyInputSlot = {
name: inputName,
link: null,
@@ -417,7 +425,17 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
const connInputIndex = node.inputs.length - 1;
// Now get the widget value.
- const [value] = node.widgets_values.splice(0, 1);
+ //
+ // Assumes the value is the first in the widget list for the
+ // case of e.g. the seed randomizer
+ // That input type adds a number widget and a combo widget so
+ // the widgets_values will have entries like
+ //
+ // [ 8, "randomize", ... ]
+ //
+ // Only care about 8 and want to skip "randomize", that's the purpose of `widgetCount`
+ const values = node.widgets_values.splice(0, widgetCount);
+ const value = values[0]
const [comfyWidgetNode, serWidgetNode] = createSerializedWidgetNode(
vanillaWorkflow,
diff --git a/src/lib/nodes/ComfyGraphNode.ts b/src/lib/nodes/ComfyGraphNode.ts
index 4e2d82f..31f8b73 100644
--- a/src/lib/nodes/ComfyGraphNode.ts
+++ b/src/lib/nodes/ComfyGraphNode.ts
@@ -312,7 +312,7 @@ export default class ComfyGraphNode extends LGraphNode {
(o as any).saveUserState = this.saveUserState
if (!this.saveUserState && (!get(uiState).isSavingToLocalStorage || get(configState).alwaysStripUserState)) {
this.stripUserState(o)
- console.warn("[ComfyGraphNode] stripUserState", this, o)
+ console.debug("[ComfyGraphNode] stripUserState", this, o)
}
}
diff --git a/src/lib/widgets.ts b/src/lib/widgets.ts
index a56e0b8..00d5399 100644
--- a/src/lib/widgets.ts
+++ b/src/lib/widgets.ts
@@ -12,7 +12,9 @@ type WidgetFactory = {
/* Input type as used by litegraph */
inputType: string,
/* Node type to instantiate */
- nodeType: string
+ nodeType: string,
+ /* Number of widgets this factory instantiates. */
+ addedWidgetCount: number
}
function getNumberDefaults(inputData: ComfyNodeDefInput, defaultStep: number): ComfyInputConfig {
@@ -49,7 +51,8 @@ const FLOAT: WidgetFactory = {
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode })
},
inputType: "number",
- nodeType: "ui/number"
+ nodeType: "ui/number",
+ addedWidgetCount: 1
}
const INT: WidgetFactory = {
@@ -58,7 +61,8 @@ const INT: WidgetFactory = {
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode })
},
nodeType: "ui/number",
- inputType: "number"
+ inputType: "number",
+ addedWidgetCount: 1
}
const STRING: WidgetFactory = {
@@ -69,7 +73,8 @@ const STRING: WidgetFactory = {
return addComfyInput(node, inputName, { type: "string", config: { defaultValue, multiline }, defaultWidgetNode: ComfyTextNode })
},
inputType: "number",
- nodeType: "ui/text"
+ nodeType: "ui/text",
+ addedWidgetCount: 1
}
const COMBO: WidgetFactory = {
@@ -82,7 +87,8 @@ const COMBO: WidgetFactory = {
return addComfyInput(node, inputName, { type: "string", config: { values: type, defaultValue }, defaultWidgetNode: ComfyComboNode })
},
inputType: "number",
- nodeType: "ui/combo"
+ nodeType: "ui/combo",
+ addedWidgetCount: 1
}
const IMAGEUPLOAD: WidgetFactory = {
@@ -90,14 +96,25 @@ const IMAGEUPLOAD: WidgetFactory = {
return addComfyInput(node, inputName, { type: "number", config: {} })
},
inputType: "COMFY_IMAGES",
- nodeType: "ui/image_upload"
+ nodeType: "ui/image_upload",
+ addedWidgetCount: 1
+}
+
+const INT_seed: WidgetFactory = {
+ ...INT,
+
+ // Adds a "randomize" combo box
+ // When converting from vanilla it should be skipped in the widgets_values
+ // array, so indicate this here
+ // litegraph really ought to key these by name instead of array indices...
+ addedWidgetCount: 2
}
export type WidgetRepository = Record
const ComfyWidgets: WidgetRepository = {
- "INT:seed": INT,
- "INT:noise_seed": INT,
+ "INT:seed": INT_seed,
+ "INT:noise_seed": INT_seed,
FLOAT,
INT,
STRING,
diff --git a/src/tests/convertVanillaWorkflowTests.ts b/src/tests/convertVanillaWorkflowTests.ts
index b904de0..1ca8a1e 100644
--- a/src/tests/convertVanillaWorkflowTests.ts
+++ b/src/tests/convertVanillaWorkflowTests.ts
@@ -10,10 +10,11 @@ import { LiteGraph } from '@litegraph-ts/core';
import type { ComfyNodeDef } from '$lib/ComfyNodeDef';
const objectInfo: Record = await import("./data/objectInfo.json")
-const json1: ComfyVanillaWorkflow = await import("./data/convertedWidgetAndPrimitiveNode.json")
+const json1: ComfyVanillaWorkflow = await import("./data/convertedWidget.json")
+const json2: ComfyVanillaWorkflow = await import("./data/convertedWidgetAndPrimitiveNode.json")
export default class convertVanillaWorkflowTests extends UnitTest {
- test__convertsPrimitiveNodeAndConvertedInput() {
+ test__convertsWidget() {
const workflow = LiteGraph.cloneObject(json1)
const attrs: WorkflowAttributes = { ...defaultWorkflowAttributes }
@@ -31,6 +32,46 @@ export default class convertVanillaWorkflowTests extends UnitTest {
expect(Object.keys(layout.allItems)).toHaveLength(10)
+ const widgets = Object.values(layout.allItems).filter(di => di.dragItem.type === "widget").map(di => di.dragItem) as WidgetLayout[];
+ expect(widgets).toHaveLength(6);
+
+ const widgetsValues = widgets.map(w => { return [w.node.type, w.node.getValue(), w.attrs.title] })
+ expect(widgetsValues).toEqual([
+ ["ui/number", 0, 'seed'],
+ ["ui/number", 20, 'steps'],
+ ["ui/number", 8.5, 'cfg'],
+ ["ui/combo", 'euler', 'sampler_name'],
+ ["ui/combo", 'normal', 'scheduler'],
+ ["ui/number", 1, 'denoise']
+ ]);
+
+ const widget = widgets.find(w => w.attrs.title === "cfg") as WidgetLayout | null;
+ expect(widget).toBeDefined();
+ expect(widget.node).toBeDefined();
+ expect(widget.node.type).toEqual("ui/number")
+ expect(widget.node.getValue()).toEqual(8.5)
+ expect(convWorkflow.graph.getNodeById(widget.node.id)).toEqual(widget.node)
+ }
+
+
+ test__convertsPrimitiveNodeAndConvertedInput() {
+ const workflow = LiteGraph.cloneObject(json2)
+ const attrs: WorkflowAttributes = { ...defaultWorkflowAttributes }
+
+ ComfyApp.knownBackendNodes["KSampler"] = {
+ nodeDef: objectInfo["KSampler"]
+ }
+
+ const converted = convertVanillaWorkflow(workflow, attrs)
+
+ expect(converted).toBeInstanceOf(Array)
+
+ const [convWorkflow, convLayout] = converted;
+
+ const layout = get(convLayout)
+
+ expect(Object.keys(layout.allItems)).toHaveLength(10)
+
const widgets = Object.values(layout.allItems).filter(di => di.dragItem.type === "widget").map(di => di.dragItem);
expect(widgets).toHaveLength(6);
diff --git a/src/tests/data/convertedWidget.json b/src/tests/data/convertedWidget.json
new file mode 100644
index 0000000..eb02439
--- /dev/null
+++ b/src/tests/data/convertedWidget.json
@@ -0,0 +1,68 @@
+{
+ "last_node_id": 1,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 1,
+ "type": "KSampler",
+ "pos": [
+ 707,
+ 502
+ ],
+ "size": {
+ "0": 315,
+ "1": 262
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "KSampler"
+ },
+ "widgets_values": [
+ 0,
+ "randomize",
+ 20,
+ 8.5,
+ "euler",
+ "normal",
+ 1
+ ]
+ }
+ ],
+ "links": [],
+ "groups": [],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
From 915265a804a7fc14f434d78f2a5e4866669a40a3 Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 18:50:27 -0500
Subject: [PATCH 11/15] Converted seed widget test
---
src/tests/convertVanillaWorkflowTests.ts | 52 ++++++++-
src/tests/data/convertedSeedWidget.json | 134 +++++++++++++++++++++++
2 files changed, 184 insertions(+), 2 deletions(-)
create mode 100644 src/tests/data/convertedSeedWidget.json
diff --git a/src/tests/convertVanillaWorkflowTests.ts b/src/tests/convertVanillaWorkflowTests.ts
index 1ca8a1e..c4bfa1f 100644
--- a/src/tests/convertVanillaWorkflowTests.ts
+++ b/src/tests/convertVanillaWorkflowTests.ts
@@ -11,7 +11,8 @@ import type { ComfyNodeDef } from '$lib/ComfyNodeDef';
const objectInfo: Record = await import("./data/objectInfo.json")
const json1: ComfyVanillaWorkflow = await import("./data/convertedWidget.json")
-const json2: ComfyVanillaWorkflow = await import("./data/convertedWidgetAndPrimitiveNode.json")
+const json2: ComfyVanillaWorkflow = await import("./data/convertedSeedWidget.json")
+const json3: ComfyVanillaWorkflow = await import("./data/convertedWidgetAndPrimitiveNode.json")
export default class convertVanillaWorkflowTests extends UnitTest {
test__convertsWidget() {
@@ -53,9 +54,56 @@ export default class convertVanillaWorkflowTests extends UnitTest {
expect(convWorkflow.graph.getNodeById(widget.node.id)).toEqual(widget.node)
}
+ test__convertsSeedWidget() {
+ const workflow = LiteGraph.cloneObject(json2)
+ const attrs: WorkflowAttributes = { ...defaultWorkflowAttributes }
+
+ ComfyApp.knownBackendNodes["KSampler"] = {
+ nodeDef: objectInfo["KSampler"]
+ }
+
+ const converted = convertVanillaWorkflow(workflow, attrs)
+
+ expect(converted).toBeInstanceOf(Array)
+
+ const [convWorkflow, convLayout] = converted;
+
+ const layout = get(convLayout)
+
+ expect(Object.keys(layout.allItems)).toHaveLength(10)
+
+ const widgets = Object.values(layout.allItems).filter(di => di.dragItem.type === "widget").map(di => di.dragItem) as WidgetLayout[];
+ expect(widgets).toHaveLength(6);
+
+ const widgetsValues = widgets.map(w => { return [w.node.type, w.node.getValue(), w.attrs.title] })
+ expect(widgetsValues).toEqual([
+ ["ui/number", 20, 'steps'],
+ ["ui/number", 8, 'cfg'],
+ ["ui/combo", 'euler', 'sampler_name'],
+ ["ui/combo", 'normal', 'scheduler'],
+ ["ui/number", 1, 'denoise'],
+ ["ui/number", 1461, 'seed']
+ ]);
+
+ const widget = widgets.find(w => w.attrs.title === "seed") as WidgetLayout | null;
+ expect(widget).toBeDefined();
+ expect(widget.node).toBeDefined();
+ expect(widget.node.type).toEqual("ui/number")
+ expect(widget.node.getValue()).toEqual(1461)
+ expect(convWorkflow.graph.getNodeById(widget.node.id)).toEqual(widget.node)
+
+ const links = widget.node.getOutputLinks(0)
+ expect(links).toHaveLength(1);
+
+ const kSampler = convWorkflow.graph.findNodesByType("KSampler")[0];
+ expect(links[0].origin_id).toEqual(widget.node.id);
+ expect(links[0].target_id).toEqual(kSampler.id);
+ expect(widget.node.outputs[0].type).toEqual("number");
+ expect(links[0].type).toEqual("number");
+ }
test__convertsPrimitiveNodeAndConvertedInput() {
- const workflow = LiteGraph.cloneObject(json2)
+ const workflow = LiteGraph.cloneObject(json3)
const attrs: WorkflowAttributes = { ...defaultWorkflowAttributes }
ComfyApp.knownBackendNodes["KSampler"] = {
diff --git a/src/tests/data/convertedSeedWidget.json b/src/tests/data/convertedSeedWidget.json
new file mode 100644
index 0000000..cc4ffc4
--- /dev/null
+++ b/src/tests/data/convertedSeedWidget.json
@@ -0,0 +1,134 @@
+{
+ "last_node_id": 2,
+ "last_link_id": 1,
+ "nodes": [
+ {
+ "id": 1,
+ "type": "KSampler",
+ "pos": [
+ 1514,
+ 348
+ ],
+ "size": [
+ 315,
+ 262
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "model",
+ "type": "MODEL",
+ "link": null
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": null
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "link": 1,
+ "widget": {
+ "name": "seed",
+ "config": [
+ "INT",
+ {
+ "default": 0,
+ "min": 0,
+ "max": 18446744073709552000
+ }
+ ]
+ },
+ "slot_index": 4
+ }
+ ],
+ "outputs": [
+ {
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "KSampler"
+ },
+ "widgets_values": [
+ 1461,
+ "randomize",
+ 20,
+ 8,
+ "euler",
+ "normal",
+ 1
+ ]
+ },
+ {
+ "id": 2,
+ "type": "PrimitiveNode",
+ "pos": [
+ 1160,
+ 420
+ ],
+ "size": {
+ "0": 210,
+ "1": 82
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 1
+ ],
+ "widget": {
+ "name": "seed",
+ "config": [
+ "INT",
+ {
+ "default": 0,
+ "min": 0,
+ "max": 18446744073709552000
+ }
+ ]
+ }
+ }
+ ],
+ "properties": {},
+ "widgets_values": [
+ 1461,
+ "randomize"
+ ]
+ }
+ ],
+ "links": [
+ [
+ 1,
+ 2,
+ 0,
+ 1,
+ 4,
+ "INT"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
From 29666a52868e7a63a49f2b14b0138142eb9b88da Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 19:09:51 -0500
Subject: [PATCH 12/15] Update README
---
README.md | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 7aaa936..e7c78fd 100644
--- a/README.md
+++ b/README.md
@@ -12,20 +12,24 @@ ComfyBox is a frontend to Stable Diffusion that lets you create custom image gen
2. Start the ComfyUI backend with `python main.py --enable-cors-header`.
3. In the folder you extracted open the `run.bat`/`run.sh` script (requires Python 3 to be on your PATH). Alternatively you can serve the contents of the folder with any web server.
+## Usage
+
+You can import your existing workflows from ComfyUI into ComfyBox by simply clicking `Load` and choosing the `.json` or `.png` with embedded metadata, or dropping either file onto the graph viewer.
+
## NOTE
This project is *still under construction* and some features are missing, be aware of the tradeoffs if you're interested in using it.
-This frontend isn't compatible with regular ComfyUI's workflow format since extra metadata is saved like panel layout, so you'll have to spend a bit of time recreating them. This project also isn't compatible with regular ComfyUI's frontend extension format, but useful extensions can be integrated into the base repo with some effort.
-
Also note that the saved workflow format is subject to change until it's been finalized after enough testing, so be prepared to lose some of your work from time to time.
## Features
-- *No-Code UI Builder* - A novel system for creating your own Stable Diffusion user interfaces from the basic components.
-- *Extension Support* - All custom ComfyUI nodes are supported out of the box.
-- *Prompt Queue* - Queue up multiple prompts without waiting for them to finish first. Inspect currently queued and executed prompts.
-- *Prompt History* - Browse through previously generated prompts and their output images/parameters.
-- *Mobile-Friendly Version* - Includes a version of the UI optimized for mobile use, while still supporting the same customized workflows of the desktop version.
+- **No-Code UI Builder** - A novel system for creating your own Stable Diffusion user interfaces from the basic components.
+- **Manage Multiple Workflows** - You can open as many workflows as you like and switch between them using tabs within the app.
+- **Use Your Existing Workflows** - Import workflows you've created in ComfyUI into ComfyBox and a new UI will be created for you.
+- **Extension Support** - All custom ComfyUI nodes are supported out of the box.
+- **Prompt Queue** - Queue up multiple prompts without waiting for them to finish first. Inspect currently queued and executed prompts.
+- **Prompt History** - Browse through previously generated prompts and their output images/parameters.
+- **Mobile-Friendly Version** - Includes a version of the UI optimized for mobile use, while still supporting the same customized workflows of the desktop version.
## Development
From 4d933127319c8dd0d66679c2ab6b1b363dc78fdc Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 19:26:33 -0500
Subject: [PATCH 13/15] Fix primitive node connected slot types
---
src/lib/convertVanillaWorkflow.ts | 27 +++++++++++++++---------
src/tests/convertVanillaWorkflowTests.ts | 4 ++++
2 files changed, 21 insertions(+), 10 deletions(-)
diff --git a/src/lib/convertVanillaWorkflow.ts b/src/lib/convertVanillaWorkflow.ts
index 1fbc021..3d13fed 100644
--- a/src/lib/convertVanillaWorkflow.ts
+++ b/src/lib/convertVanillaWorkflow.ts
@@ -272,7 +272,7 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
const widgetLayout = layoutState.addWidget(group, comfyWidgetNode)
widgetLayout.attrs.title = mainOutput.name;
- // Rewrite links to point to the new widget node
+ // Follow the existing links on the original node and do some cleanup
const newLinkOutputSlot = serWidgetNode.outputs.findIndex(o => o.name === comfyWidgetNode.outputSlotName)
if (newLinkOutputSlot !== -1) {
const newLinkOutput = serWidgetNode.outputs[newLinkOutputSlot];
@@ -280,21 +280,28 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
for (const linkID of mainOutput.links) {
const link = vanillaWorkflow.links.find(l => l[0] === linkID)
if (link) {
+ // Rewrite links to point to the new widget node
link[1] = serWidgetNode.id; // origin node ID
link[2] = newLinkOutputSlot; // origin node slot
newLinkOutput.links ||= []
newLinkOutput.links.push(linkID)
+ // Look up the node the link was connected to.
+ const targetNode = vanillaWorkflow.nodes.find(n => n.id === link[3]) // target node ID
+ const foundInput = targetNode != null ? targetNode.inputs[link[4]] : null // target node slot
+
+ // Make sure that the input type for the connected inputs is correct.
+ // ComfyUI seems to set them to the input def type instead of the litegraph type.
+ // For example a "number" input gets changed to type "INT" or "FLOAT"
+ link[5] = widgetInputType // link data type
+ if (foundInput != null) {
+ foundInput.type = widgetInputType;
+ }
+
// Change the title of the widget to the name of the first input connected to
- if (foundTitle == null) {
- const targetNode = vanillaWorkflow.nodes.find(n => n.id === link[3]) // target node ID
- if (targetNode != null) {
- const foundInput = targetNode.inputs[link[4]] // target node slot
- if (foundInput != null && foundInput.name) {
- foundTitle = foundInput.name;
- widgetLayout.attrs.title = foundTitle;
- }
- }
+ if (foundTitle == null && foundInput != null && foundInput.name) {
+ foundTitle = foundInput.name;
+ widgetLayout.attrs.title = foundTitle;
}
}
}
diff --git a/src/tests/convertVanillaWorkflowTests.ts b/src/tests/convertVanillaWorkflowTests.ts
index c4bfa1f..6f9ba05 100644
--- a/src/tests/convertVanillaWorkflowTests.ts
+++ b/src/tests/convertVanillaWorkflowTests.ts
@@ -98,7 +98,11 @@ export default class convertVanillaWorkflowTests extends UnitTest {
const kSampler = convWorkflow.graph.findNodesByType("KSampler")[0];
expect(links[0].origin_id).toEqual(widget.node.id);
expect(links[0].target_id).toEqual(kSampler.id);
+
expect(widget.node.outputs[0].type).toEqual("number");
+
+ const targetNode = widget.node.getOutputNodes(0)[0]
+ expect(targetNode.inputs[links[0].target_slot].type).toEqual("number")
expect(links[0].type).toEqual("number");
}
From 970946b51494828e78417bf80256d5d534e3d2d9 Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 19:43:32 -0500
Subject: [PATCH 14/15] Make sure new input slots are serializable
---
src/lib/convertVanillaWorkflow.ts | 8 ++++++--
src/tests/convertVanillaWorkflowTests.ts | 2 ++
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/lib/convertVanillaWorkflow.ts b/src/lib/convertVanillaWorkflow.ts
index 3d13fed..6f73e7b 100644
--- a/src/lib/convertVanillaWorkflow.ts
+++ b/src/lib/convertVanillaWorkflow.ts
@@ -296,6 +296,7 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
link[5] = widgetInputType // link data type
if (foundInput != null) {
foundInput.type = widgetInputType;
+ (foundInput as IComfyInputSlot).serialize = true; // IMPORTANT!!!
}
// Change the title of the widget to the name of the first input connected to
@@ -305,7 +306,10 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
}
}
}
- // Remove links on the old node so they won't be double-removed when it's pruned
+
+ // Remove links on the old node so they won't be double-removed when
+ // it's pruned (removeSerializedNode will remove any links still
+ // connected to other inputs, but we want to keep the ones we rewrote)
mainOutput.links = []
}
else {
@@ -423,7 +427,7 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
config: inputOpts,
defaultWidgetNode: null,
widgetNodeType,
- serialize: true,
+ serialize: true, // IMPORTANT!
properties: {}
}
diff --git a/src/tests/convertVanillaWorkflowTests.ts b/src/tests/convertVanillaWorkflowTests.ts
index 6f9ba05..9dc3cb7 100644
--- a/src/tests/convertVanillaWorkflowTests.ts
+++ b/src/tests/convertVanillaWorkflowTests.ts
@@ -8,6 +8,7 @@ import layoutStates, { defaultWorkflowAttributes, type IDragItem, type WidgetLay
import ComfyApp from '$lib/components/ComfyApp';
import { LiteGraph } from '@litegraph-ts/core';
import type { ComfyNodeDef } from '$lib/ComfyNodeDef';
+import type IComfyInputSlot from '$lib/IComfyInputSlot';
const objectInfo: Record = await import("./data/objectInfo.json")
const json1: ComfyVanillaWorkflow = await import("./data/convertedWidget.json")
@@ -103,6 +104,7 @@ export default class convertVanillaWorkflowTests extends UnitTest {
const targetNode = widget.node.getOutputNodes(0)[0]
expect(targetNode.inputs[links[0].target_slot].type).toEqual("number")
+ expect((targetNode.inputs[links[0].target_slot] as IComfyInputSlot).serialize).toEqual(true)
expect(links[0].type).toEqual("number");
}
From fa16d1ee38f0b1a8c034fb754393691d11f0247a Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 20:43:47 -0500
Subject: [PATCH 15/15] Fix mobile
---
README.md | 4 ++-
src/lib/components/ComfyQueue.svelte | 46 +++++++++++++++++++---------
src/lib/convertVanillaWorkflow.ts | 10 +++---
src/lib/stores/queueState.ts | 18 ++++++++++-
src/mobile/routes/subworkflow.svelte | 3 +-
src/scss/global.scss | 3 +-
6 files changed, 60 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
index e7c78fd..889f090 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,9 @@ ComfyBox is a frontend to Stable Diffusion that lets you create custom image gen
## Usage
-You can import your existing workflows from ComfyUI into ComfyBox by simply clicking `Load` and choosing the `.json` or `.png` with embedded metadata, or dropping either file onto the graph viewer.
+A preconfigured workflow is included for the most common txt2img and img2img use cases, so all it takes to start generating is clicking `Load Default` to load the default workflow and then `Queue Prompt`.
+
+You can import your existing workflows from ComfyUI into ComfyBox by clicking `Load` and choosing the `.json` or `.png` with embedded metadata, or dropping either file onto the graph viewer.
## NOTE
diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte
index 6c4d320..50e2c0d 100644
--- a/src/lib/components/ComfyQueue.svelte
+++ b/src/lib/components/ComfyQueue.svelte
@@ -22,12 +22,14 @@
let queueCompleted: Writable | null = null;
let queueList: HTMLDivElement | null = null;
+ type QueueUIEntryStatus = QueueEntryStatus | "pending" | "running";
+
type QueueUIEntry = {
entry: QueueEntry,
message: string,
submessage: string,
date?: string,
- status: QueueEntryStatus | "pending" | "running",
+ status: QueueUIEntryStatus,
images?: string[], // URLs
details?: string // shown in a tooltip on hover
}
@@ -39,21 +41,29 @@
}
let mode: QueueItemType = "queue";
+ let changed = true;
function switchMode(newMode: QueueItemType) {
- const changed = mode !== newMode
+ changed = mode !== newMode
mode = newMode
- if (changed)
+ if (changed) {
+ _queuedEntries = []
+ _runningEntries = []
_entries = []
+ }
}
+ let _queuedEntries: QueueUIEntry[] = []
+ let _runningEntries: QueueUIEntry[] = []
let _entries: QueueUIEntry[] = []
- $: if (mode === "queue" && $queuePending && $queuePending.length != _entries.length) {
+ $: if (mode === "queue" && (changed || ($queuePending && $queuePending.length != _queuedEntries.length))) {
updateFromQueue();
+ changed = false;
}
- else if (mode === "history" && $queueCompleted && $queueCompleted.length != _entries.length) {
+ else if (mode === "history" && (changed || ($queueCompleted && $queueCompleted.length != _entries.length))) {
updateFromHistory();
+ changed = false;
}
function formatDate(date: Date): string {
@@ -62,7 +72,7 @@
return [time, day].join(", ")
}
- function convertEntry(entry: QueueEntry): QueueUIEntry {
+ function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
let date = entry.finishedAt || entry.queuedAt;
let dateStr = null;
if (date) {
@@ -93,13 +103,13 @@
message,
submessage,
date: dateStr,
- status: "pending",
+ status,
images: []
}
}
- function convertPendingEntry(entry: QueueEntry): QueueUIEntry {
- const result = convertEntry(entry);
+ function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
+ const result = convertEntry(entry, status);
const thumbnails = entry.extraData?.thumbnails
if (thumbnails) {
@@ -110,8 +120,7 @@
}
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
- const result = convertEntry(entry.entry);
- result.status = entry.status;
+ const result = convertEntry(entry.entry, entry.status);
const images = Object.values(entry.entry.outputs).flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
@@ -128,12 +137,15 @@
}
async function updateFromQueue() {
- _entries = $queuePending.map(convertPendingEntry).reverse(); // newest entries appear at the top
+ // newest entries appear at the top
+ _queuedEntries = $queuePending.map((e) => convertPendingEntry(e, "pending")).reverse();
+ _runningEntries = $queueRunning.map((e) => convertPendingEntry(e, "running")).reverse();
+ _entries = [..._queuedEntries, ..._runningEntries]
if (queueList) {
await tick(); // Wait for list size to be recalculated
queueList.scroll({ top: queueList.scrollHeight })
}
- console.warn("[ComfyQueue] BUILDQUEUE", _entries, $queuePending)
+ console.warn("[ComfyQueue] BUILDQUEUE", _entries, $queuePending, $queueRunning)
}
async function updateFromHistory() {
@@ -368,6 +380,10 @@
&:hover:not(:has(img:hover)) {
cursor: pointer;
background: var(--block-background-fill);
+
+ &.running {
+ background: var(--comfy-accent-soft);
+ }
}
&.success {
@@ -382,10 +398,10 @@
color: var(--comfy-disable-textbox-text-color);
}
&.running {
- /* background: lightblue; */
+ background: var(--block-background-fill);
+ border: 3px dashed var(--neutral-500);
}
&.pending, &.unknown {
- /* background: orange; */
}
}
diff --git a/src/lib/convertVanillaWorkflow.ts b/src/lib/convertVanillaWorkflow.ts
index 6f73e7b..4b4ca7a 100644
--- a/src/lib/convertVanillaWorkflow.ts
+++ b/src/lib/convertVanillaWorkflow.ts
@@ -159,7 +159,6 @@ function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) {
link[3] = getNodeID(link[3])
}
-
// Recurse!
for (const node of vanillaWorkflow.nodes) {
if (node.type === "graph/subgraph") {
@@ -170,7 +169,8 @@ function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) {
/*
* Returns [nodeType, inputType, addedWidgetCount] for a config type, like "FLOAT" -> ["ui/number", "number", 1]
- * For "INT:seed" it's ["ui/number", "number", 2] since that type adds a randomizer combo widget
+ * For "INT:seed" it's ["ui/number", "number", 2] since that type adds a randomizer combo widget,
+ * so there will be 2 total widgets
*/
function getWidgetTypesFromConfig(inputName: string, inputType: ComfyNodeDefInputType): [string, SlotType, number] | null {
let widgetNodeType = null;
@@ -266,6 +266,7 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
widgetNodeType,
value);
+ // Set the UI node's min/max/step from the node def
configureWidgetNodeProperties(serWidgetNode, widgetOpts)
let foundTitle = null;
@@ -276,7 +277,6 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
const newLinkOutputSlot = serWidgetNode.outputs.findIndex(o => o.name === comfyWidgetNode.outputSlotName)
if (newLinkOutputSlot !== -1) {
const newLinkOutput = serWidgetNode.outputs[newLinkOutputSlot];
- // TODO other links need pruning?
for (const linkID of mainOutput.links) {
const link = vanillaWorkflow.links.find(l => l[0] === linkID)
if (link) {
@@ -293,6 +293,8 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
// Make sure that the input type for the connected inputs is correct.
// ComfyUI seems to set them to the input def type instead of the litegraph type.
// For example a "number" input gets changed to type "INT" or "FLOAT"
+ // Also ensure the input is marked for serialization, else there
+ // will be random prompt validation errors on the backend
link[5] = widgetInputType // link data type
if (foundInput != null) {
foundInput.type = widgetInputType;
@@ -343,8 +345,6 @@ function removeSerializedNode(vanillaWorkflow: SerializedLGraph, node: Serialize
/*
* Converts a workflow saved with vanilla ComfyUI into a ComfyBox workflow,
* adding UI nodes for each widget.
- *
- * TODO: test this!
*/
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): [ComfyWorkflow, WritableLayoutStateStore] {
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
diff --git a/src/lib/stores/queueState.ts b/src/lib/stores/queueState.ts
index 4f563c2..979d129 100644
--- a/src/lib/stores/queueState.ts
+++ b/src/lib/stores/queueState.ts
@@ -181,6 +181,19 @@ function findEntryInPending(promptID: PromptID): [number, QueueEntry | null, Wri
return [-1, null, null]
}
+function moveToRunning(index: number, queue: Writable) {
+ const state = get(store)
+
+ const entry = get(queue)[index];
+ console.debug("[queueState] Move to running", entry.promptID, index)
+ // entry.startedAt = new Date() // Now
+ queue.update(qp => { qp.splice(index, 1); return qp });
+ state.queueRunning.update(qr => { qr.push(entry); return qr })
+
+ state.isInterrupting = false;
+ store.set(state)
+}
+
function moveToCompleted(index: number, queue: Writable, status: QueueEntryStatus, message?: string, error?: string) {
const state = get(store)
@@ -298,9 +311,12 @@ function executionStart(promptID: PromptID) {
const [index, entry, queue] = findEntryInPending(promptID);
if (entry == null) {
const entry = createNewQueueEntry(promptID);
- s.queuePending.update(qp => { qp.push(entry); return qp })
+ s.queueRunning.update(qr => { qr.push(entry); return qr })
console.debug("[queueState] ADD PROMPT", promptID)
}
+ else {
+ moveToRunning(index, queue)
+ }
s.isInterrupting = false;
return s
})
diff --git a/src/mobile/routes/subworkflow.svelte b/src/mobile/routes/subworkflow.svelte
index 9f52163..a05ac90 100644
--- a/src/mobile/routes/subworkflow.svelte
+++ b/src/mobile/routes/subworkflow.svelte
@@ -2,9 +2,9 @@
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
import type ComfyApp from "$lib/components/ComfyApp";
- import type { ComfyWorkflow } from "$lib/components/ComfyApp";
import { writable, type Writable } from "svelte/store";
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
+ import workflowState, { type ComfyWorkflow } from "$lib/stores/workflowState";
export let subworkflowID: number = -1;
export let app: ComfyApp
@@ -13,6 +13,7 @@
let workflow: ComfyWorkflow | null = null
let layoutState: WritableLayoutStateStore | null = null;
+ $: workflow = $workflowState.activeWorkflow;
$: layoutState = workflow ? workflow.layout : null;
diff --git a/src/scss/global.scss b/src/scss/global.scss
index ae2abd8..ed2d51c 100644
--- a/src/scss/global.scss
+++ b/src/scss/global.scss
@@ -31,6 +31,7 @@ body {
--comfy-splitpanes-background-fill: var(--secondary-100);
--comfy-splitpanes-background-fill-hover: var(--secondary-300);
--comfy-splitpanes-background-fill-active: var(--secondary-400);
+ --comfy-dropdown-list-background: white;
--comfy-dropdown-item-color-hover: white;
--comfy-dropdown-item-background-hover: var(--neutral-400);
--comfy-dropdown-item-color-active: var(--neutral-100);
@@ -107,7 +108,7 @@ hr {
color: var(--panel-border-color);
}
-input, textarea {
+input:not(input[type=radio]), textarea {
border-radius: 0 !important;
}