Prompt serializer and test fixes

This commit is contained in:
space-nuko
2023-05-20 21:42:38 -05:00
parent 8d031120f7
commit 09e806bd3e
17 changed files with 196 additions and 106 deletions

View File

@@ -1,6 +1,11 @@
{ {
"createdBy": "ComfyBox", "createdBy": "ComfyBox",
"version": 1, "version": 1,
"attrs": {
"title": "Default",
"queuePromptButtonName": "Queue txt2img",
"queuePromptButtonRunWorkflow": false
},
"workflow": { "workflow": {
"last_node_id": 0, "last_node_id": 0,
"last_link_id": 0, "last_link_id": 0,
@@ -25709,10 +25714,6 @@
], ],
"parent": "eae32e42-1ccc-4a4a-923f-7ab4ccdac97a" "parent": "eae32e42-1ccc-4a4a-923f-7ab4ccdac97a"
} }
},
"attrs": {
"queuePromptButtonName": "Queue txt2img",
"queuePromptButtonRunWorkflow": false
} }
}, },
"canvas": { "canvas": {

View File

@@ -118,7 +118,6 @@ export default class ComfyGraph extends LGraph {
} }
if (get(uiState).autoAddUI) { if (get(uiState).autoAddUI) {
console.warn("ADD", node.type, options)
if (!("svelteComponentType" in node) && options.addedBy == null) { if (!("svelteComponentType" in node) && options.addedBy == null) {
console.debug("[ComfyGraph] AutoAdd UI") console.debug("[ComfyGraph] AutoAdd UI")
const comfyNode = node as ComfyGraphNode; const comfyNode = node as ComfyGraphNode;
@@ -167,10 +166,10 @@ export default class ComfyGraph extends LGraph {
override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) { override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
selectionState.clear(); // safest option selectionState.clear(); // safest option
if (node.getRootGraph() != null && !this._is_subgraph && this.workflowID != null) { if (!this._is_subgraph && this.workflowID != null) {
const layoutState = get(layoutStates).all[this.workflowID] const layoutState = get(layoutStates).all[this.workflowID]
if (layoutState === null) { if (layoutState === null) {
throw new Error(`LGraph with workflow missing layout! ${this.workflowID}`) throw new Error(`ComfyGraph with workflow missing layout! ${this.workflowID}`)
} }
layoutState.nodeRemoved(node, options); layoutState.nodeRemoved(node, options);

View File

@@ -42,7 +42,7 @@ import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer"; import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
import selectionState from "$lib/stores/selectionState"; import selectionState from "$lib/stores/selectionState";
import layoutStates from "$lib/stores/layoutStates"; import layoutStates from "$lib/stores/layoutStates";
import { ComfyWorkflow } from "$lib/stores/workflowState"; import { ComfyWorkflow, type WorkflowAttributes } from "$lib/stores/workflowState";
import workflowState from "$lib/stores/workflowState"; import workflowState from "$lib/stores/workflowState";
export const COMFYBOX_SERIAL_VERSION = 1; export const COMFYBOX_SERIAL_VERSION = 1;
@@ -80,8 +80,8 @@ export type SerializedAppState = {
commitHash?: string, commitHash?: string,
/** Graph state */ /** Graph state */
workflow: SerializedLGraph, workflow: SerializedLGraph,
/** Workflow name */ /** Workflow attributes */
workflowName: string, attrs: WorkflowAttributes,
/** UI state */ /** UI state */
layout: SerializedLayoutState, layout: SerializedLayoutState,
/** Position/offset of the canvas at the time of saving */ /** Position/offset of the canvas at the time of saving */
@@ -165,7 +165,7 @@ export default class ComfyApp {
async setup(): Promise<void> { async setup(): Promise<void> {
if (get(this.alreadySetup)) { if (get(this.alreadySetup)) {
console.error("Already setup") console.log("Already setup")
return; return;
} }
@@ -241,7 +241,7 @@ export default class ComfyApp {
if (layoutState == null) if (layoutState == null)
throw new Error("Workflow has no layout!") throw new Error("Workflow has no layout!")
const { graph, layout } = workflow.serialize(layoutState); const { graph, layout, attrs } = workflow.serialize(layoutState);
const canvas = this.lCanvas.serialize(); const canvas = this.lCanvas.serialize();
return { return {
@@ -249,6 +249,7 @@ export default class ComfyApp {
version: COMFYBOX_SERIAL_VERSION, version: COMFYBOX_SERIAL_VERSION,
commitHash: __GIT_COMMIT_HASH__, commitHash: __GIT_COMMIT_HASH__,
workflow: graph, workflow: graph,
attrs,
layout, layout,
canvas canvas
} }
@@ -566,7 +567,7 @@ export default class ComfyApp {
} }
} }
if (get(workflow.layout).attrs.queuePromptButtonRunWorkflow) { if (workflow.attrs.queuePromptButtonRunWorkflow) {
// Hold control to queue at the front // Hold control to queue at the front
const num = this.ctrlDown ? -1 : 0; const num = this.ctrlDown ? -1 : 0;
this.queuePrompt(num, 1); this.queuePrompt(num, 1);

View File

@@ -35,7 +35,15 @@ export function isActiveBackendNode(node: LGraphNode, tag: string | null = null)
if (!(node as any).isBackendNode) if (!(node as any).isBackendNode)
return false; return false;
return isActiveNode(node, tag); if (!isActiveNode(node, tag))
return false;
// Make sure this node is not contained in an inactive subgraph, even if the
// node itself is active
if (node.is(Subgraph) && !Array.from(node.iterateParentNodes()).every(n => isActiveNode(n, tag)))
return false;
return true;
} }
export class UpstreamNodeLocator { export class UpstreamNodeLocator {
@@ -166,7 +174,7 @@ export default class ComfyPromptSerializer {
// We don't check tags for non-backend nodes. // We don't check tags for non-backend nodes.
// Just check for node inactivity (so you can toggle groups of // Just check for node inactivity (so you can toggle groups of
// tagged frontend nodes on/off) // tagged frontend nodes on/off)
if (inputNode && inputNode.mode === NodeMode.NEVER) { if (inputNode && inputNode.mode !== NodeMode.ALWAYS) {
console.debug("Skipping inactive node", inputNode) console.debug("Skipping inactive node", inputNode)
continue; continue;
} }
@@ -248,6 +256,8 @@ export default class ComfyPromptSerializer {
const inputs = this.serializeInputValues(node); const inputs = this.serializeInputValues(node);
const links = this.serializeBackendLinks(node, tag); const links = this.serializeBackendLinks(node, tag);
console.warn("OUTPUT", node.id, node.comfyClass, node.mode)
output[String(node.id)] = { output[String(node.id)] = {
inputs: { ...inputs, ...links }, inputs: { ...inputs, ...links },
class_type: node.comfyClass, class_type: node.comfyClass,

View File

@@ -4,6 +4,7 @@
import { LGraphNode } from "@litegraph-ts/core" import { LGraphNode } from "@litegraph-ts/core"
import { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec, type WritableLayoutStateStore } from "$lib/stores/layoutStates" import { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec, type WritableLayoutStateStore } from "$lib/stores/layoutStates"
import uiState from "$lib/stores/uiState" import uiState from "$lib/stores/uiState"
import workflowState from "$lib/stores/workflowState"
import layoutStates from "$lib/stores/layoutStates" import layoutStates from "$lib/stores/layoutStates"
import selectionState from "$lib/stores/selectionState" import selectionState from "$lib/stores/selectionState"
import { get, type Writable, writable } from "svelte/store" import { get, type Writable, writable } from "svelte/store"
@@ -127,7 +128,10 @@
if (spec.location !== "workflow") if (spec.location !== "workflow")
return false; return false;
return spec.name in $layoutState.attrs if (workflow == null)
return false;
return spec.name in workflow.attrs
} }
function getAttribute(target: IDragItem, spec: AttributesSpec): any { function getAttribute(target: IDragItem, spec: AttributesSpec): any {
@@ -240,7 +244,10 @@
} }
function getWorkflowAttribute(spec: AttributesSpec): any { function getWorkflowAttribute(spec: AttributesSpec): any {
let value = $layoutState.attrs[spec.name] if (workflow == null)
throw new Error("Active workflow is null!");
let value = workflow.attrs[spec.name]
if (value == null) if (value == null)
value = spec.defaultValue value = spec.defaultValue
else if (spec.serialize) else if (spec.serialize)
@@ -253,17 +260,20 @@
if (!spec.editable) if (!spec.editable)
return; return;
if (workflow == null)
throw new Error("Active workflow is null!");
const name = spec.name const name = spec.name
// console.warn("[ComfyProperties] updateWorkflowAttribute", name, value) // console.warn("[ComfyProperties] updateWorkflowAttribute", name, value)
const prevValue = value const prevValue = value
$layoutState.attrs[name] = value workflow.attrs[name] = value
$layoutState = $layoutState $workflowState = $workflowState;
if (spec.onChanged) if (spec.onChanged)
spec.onChanged($layoutState, value, prevValue) spec.onChanged($layoutState, value, prevValue)
if (spec.refreshPanelOnChange) // if (spec.refreshPanelOnChange)
doRefreshPanel() doRefreshPanel()
} }

View File

@@ -13,6 +13,7 @@
import { tick } from "svelte"; import { tick } from "svelte";
import Modal from "./Modal.svelte"; import Modal from "./Modal.svelte";
import DropZone from "./DropZone.svelte"; import DropZone from "./DropZone.svelte";
import workflowState from "$lib/stores/workflowState";
export let app: ComfyApp; export let app: ComfyApp;
@@ -71,10 +72,17 @@
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxSubgraphs; const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxSubgraphs;
let message = "Prompt"; let message = "Prompt";
if (subgraphs?.length > 0) if (entry.workflowID != null) {
message = `Prompt: ${subgraphs.join(', ')}` const workflow = workflowState.getWorkflow(entry.workflowID);
if (workflow != null && workflow.attrs.title) {
message = `Workflow: ${workflow.attrs.title}`
}
if (subgraphs?.length > 0)
message += ` (${subgraphs.join(', ')})`
}
let submessage = `Nodes: ${Object.keys(entry.prompt).length}` let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
if (Object.keys(entry.outputs).length > 0) { if (Object.keys(entry.outputs).length > 0) {
const imageCount = Object.values(entry.outputs).flatMap(o => o.images).length const imageCount = Object.values(entry.outputs).flatMap(o => o.images).length
submessage = `Images: ${imageCount}` submessage = `Images: ${imageCount}`
@@ -84,7 +92,7 @@
entry, entry,
message, message,
submessage, submessage,
dateStr, date: dateStr,
status: "pending", status: "pending",
images: [] images: []
} }
@@ -387,7 +395,7 @@
&.all_cached, &.interrupted { &.all_cached, &.interrupted {
filter: brightness(80%); filter: brightness(80%);
color: var(--neutral-300); color: var(--comfy-accent-soft);
} }
} }

View File

@@ -10,7 +10,7 @@
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import ComfyProperties from "./ComfyProperties.svelte"; import ComfyProperties from "./ComfyProperties.svelte";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import workflowState from "$lib/stores/workflowState"; import workflowState, { ComfyWorkflow } from "$lib/stores/workflowState";
import selectionState from "$lib/stores/selectionState"; import selectionState from "$lib/stores/selectionState";
import type ComfyApp from './ComfyApp'; import type ComfyApp from './ComfyApp';
import { onMount } from "svelte"; import { onMount } from "svelte";
@@ -19,7 +19,7 @@
export let app: ComfyApp; export let app: ComfyApp;
export let uiTheme: string = "gradio-dark" // TODO config export let uiTheme: string = "gradio-dark" // TODO config
let layoutState: WritableLayoutStateStore | null = null; let workflow: ComfyWorkflow | null = null;
let containerElem: HTMLDivElement; let containerElem: HTMLDivElement;
let resizeTimeout: NodeJS.Timeout | null; let resizeTimeout: NodeJS.Timeout | null;
@@ -29,7 +29,7 @@
let appSetupPromise: Promise<void> = null; let appSetupPromise: Promise<void> = null;
$: layoutState = $workflowState.activeWorkflow?.layout; $: workflow = $workflowState.activeWorkflow;
onMount(async () => { onMount(async () => {
appSetupPromise = app.setup().then(() => { appSetupPromise = app.setup().then(() => {
@@ -189,10 +189,10 @@
<button class="workflow-tab" <button class="workflow-tab"
class:selected={index === $workflowState.activeWorkflowIdx} class:selected={index === $workflowState.activeWorkflowIdx}
on:click={() => app.setActiveWorkflow(index)}> on:click={() => app.setActiveWorkflow(index)}>
<span class="workflow-tab-title">{workflow.title}</span> <span class="workflow-tab-title">{workflow.attrs.title}</span>
<button class="workflow-close-button" <button class="workflow-close-button"
on:click={(e) => closeWorkflow(e, index)}> on:click={(e) => closeWorkflow(e, index)}>
X
</button> </button>
</button> </button>
{/each} {/each}
@@ -200,9 +200,9 @@
<div id="bottombar"> <div id="bottombar">
<div class="bottombar-content"> <div class="bottombar-content">
<div class="left"> <div class="left">
{#if layoutState != null && $layoutState.attrs.queuePromptButtonName != ""} {#if workflow != null && workflow.attrs.queuePromptButtonName != ""}
<Button variant="primary" disabled={!$alreadySetup} on:click={queuePrompt}> <Button variant="primary" disabled={!$alreadySetup} on:click={queuePrompt}>
{$layoutState.attrs.queuePromptButtonName} {workflow.attrs.queuePromptButtonName}
</Button> </Button>
{/if} {/if}
<Button variant="secondary" disabled={!$alreadySetup} on:click={toggleGraph}> <Button variant="secondary" disabled={!$alreadySetup} on:click={toggleGraph}>
@@ -395,11 +395,12 @@
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
border-radius: 50%; border-radius: 50%;
opacity: 50%;
background: var(--neutral-500); background: var(--neutral-500);
color: var(--neutral-300); color: var(--neutral-300);
&:hover { &:hover {
background: var(--neutral-400); opacity: 100%;
color: var(--neutral-100); color: var(--neutral-100);
} }
} }

View File

@@ -3,7 +3,11 @@ import type { SerializedAppState } from "./components/ComfyApp"
const blankGraph: SerializedAppState = { const blankGraph: SerializedAppState = {
createdBy: "ComfyBox", createdBy: "ComfyBox",
version: 1, version: 1,
workflowName: "New Workflow", attrs: {
title: "New Workflow",
queuePromptButtonName: "Queue Prompt",
queuePromptButtonRunWorkflow: true
},
workflow: { workflow: {
last_node_id: 0, last_node_id: 0,
last_link_id: 0, last_link_id: 0,
@@ -17,10 +21,10 @@ const blankGraph: SerializedAppState = {
layout: { layout: {
root: null, root: null,
allItems: {}, allItems: {},
attrs: { },
queuePromptButtonName: "Queue Prompt", canvas: {
queuePromptButtonRunWorkflow: true offset: [0, 0],
} scale: 1
} }
} }

View File

@@ -29,13 +29,10 @@ export default class ComfyConfigureQueuePromptButton extends ComfyGraphNode {
throw new Error(`Could not find layout attached to this node! ${this.id}`) throw new Error(`Could not find layout attached to this node! ${this.id}`)
} }
this.layoutState.update(state => { if (typeof param === "string")
if (typeof param === "string") this.workflow.attrs.queuePromptButtonName = param || ""
state.attrs.queuePromptButtonName = param || "" else if (typeof param === "object" && "buttonName" in param)
else if (typeof param === "object" && "buttonName" in param) this.workflow.attrs.queuePromptButtonName = param.buttonName || ""
state.attrs.queuePromptButtonName = param.buttonName || ""
return state
})
} }
} }
} }

View File

@@ -10,6 +10,7 @@ import { get } from "svelte/store";
import configState from "$lib/stores/configState"; import configState from "$lib/stores/configState";
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates"; import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
import layoutStates from "$lib/stores/layoutStates"; import layoutStates from "$lib/stores/layoutStates";
import workflowStateStore, { ComfyWorkflow } from "$lib/stores/workflowState";
export type DefaultWidgetSpec = { export type DefaultWidgetSpec = {
defaultWidgetNode: new (name?: string) => ComfyWidgetNode, defaultWidgetNode: new (name?: string) => ComfyWidgetNode,
@@ -106,6 +107,10 @@ export default class ComfyGraphNode extends LGraphNode {
return layoutStates.getLayoutByNode(this); return layoutStates.getLayoutByNode(this);
} }
get workflow(): ComfyWorkflow | null {
return workflowStateStore.getWorkflowByNode(this);
}
constructor(title?: string) { constructor(title?: string) {
super(title) super(title)
this.addProperty("tags", [], "array") this.addProperty("tags", [], "array")

View File

@@ -8,6 +8,7 @@ import { v4 as uuidv4 } from "uuid";
import type { ComfyWidgetNode } from '$lib/nodes/widgets'; import type { ComfyWidgetNode } from '$lib/nodes/widgets';
import type { ComfyWorkflow, WorkflowInstID } from '$lib/components/ComfyApp'; import type { ComfyWorkflow, WorkflowInstID } from '$lib/components/ComfyApp';
import type ComfyGraph from '$lib/ComfyGraph'; import type ComfyGraph from '$lib/ComfyGraph';
import type { WorkflowAttributes } from './workflowState';
function isComfyWidgetNode(node: LGraphNode): node is ComfyWidgetNode { function isComfyWidgetNode(node: LGraphNode): node is ComfyWidgetNode {
return "svelteComponentType" in node return "svelteComponentType" in node
@@ -31,24 +32,6 @@ type DragItemEntry = {
parent: IDragItem | null parent: IDragItem | null
} }
/*
* Global workflow attributes
*/
export type LayoutAttributes = {
/*
* Name of the "Queue Prompt" button. Set to blank to hide the button.
*/
queuePromptButtonName: string,
/*
* If true, clicking the "Queue Prompt" button will run the default
* subgraph. Set this to false if you need special behavior before running
* any subgraphs, and instead use the `onDefaultQueueAction` event of the
* Comfy.QueueEvents node.
*/
queuePromptButtonRunWorkflow: boolean,
}
/* /*
* Keeps track of the tree of UI components - widgets and the containers that * Keeps track of the tree of UI components - widgets and the containers that
* group them together. * group them together.
@@ -82,11 +65,6 @@ export type LayoutState = {
* If true, the right-click context menu is open * If true, the right-click context menu is open
*/ */
isMenuOpen: boolean, isMenuOpen: boolean,
/*
* Global workflow attributes
*/
attrs: LayoutAttributes
} }
/** /**
@@ -185,7 +163,7 @@ export type AttributesSpec = {
* - "widget": inside IDragNode.attrs * - "widget": inside IDragNode.attrs
* - "nodeProps": inside LGraphNode.properties * - "nodeProps": inside LGraphNode.properties
* - "nodeVars": an instance variable directly on an LGraphNode * - "nodeVars": an instance variable directly on an LGraphNode
* - "workflow": inside $layoutState.attrs * - "workflow": inside $workflowState.activeWorkflow.attrs
*/ */
location: "widget" | "nodeProps" | "nodeVars" | "workflow" location: "widget" | "nodeProps" | "nodeVars" | "workflow"
@@ -601,7 +579,7 @@ export { ALL_ATTRIBUTES };
// TODO Should be nested by category for name uniqueness? // TODO Should be nested by category for name uniqueness?
const defaultWidgetAttributes: Attributes = {} as any const defaultWidgetAttributes: Attributes = {} as any
const defaultWorkflowAttributes: LayoutAttributes = {} as any export const defaultWorkflowAttributes: WorkflowAttributes = {} as any
for (const cat of Object.values(ALL_ATTRIBUTES)) { for (const cat of Object.values(ALL_ATTRIBUTES)) {
for (const spec of Object.values(cat.specs)) { for (const spec of Object.values(cat.specs)) {
if (spec.defaultValue != null) { if (spec.defaultValue != null) {
@@ -697,7 +675,6 @@ type LayoutStateOps = {
export type SerializedLayoutState = { export type SerializedLayoutState = {
root: DragItemID | null, root: DragItemID | null,
allItems: Record<DragItemID, SerializedDragEntry>, allItems: Record<DragItemID, SerializedDragEntry>,
attrs: LayoutAttributes
} }
export type SerializedDragEntry = { export type SerializedDragEntry = {
@@ -726,9 +703,6 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
allItemsByNode: {}, allItemsByNode: {},
isMenuOpen: false, isMenuOpen: false,
isConfiguring: true, isConfiguring: true,
attrs: {
...defaultWorkflowAttributes
}
}) })
function clear() { function clear() {
@@ -738,9 +712,6 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
allItemsByNode: {}, allItemsByNode: {},
isMenuOpen: false, isMenuOpen: false,
isConfiguring: true, isConfiguring: true,
attrs: {
...defaultWorkflowAttributes
}
}) })
} }
@@ -1063,9 +1034,6 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
allItemsByNode: {}, allItemsByNode: {},
isMenuOpen: false, isMenuOpen: false,
isConfiguring: false, isConfiguring: false,
attrs: {
...defaultWorkflowAttributes
}
}) })
const root = addContainer(null, { direction: "horizontal", title: "" }); const root = addContainer(null, { direction: "horizontal", title: "" });
@@ -1100,7 +1068,6 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
return { return {
root: state.root?.id, root: state.root?.id,
allItems, allItems,
attrs: state.attrs
} }
} }
@@ -1156,7 +1123,6 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
allItemsByNode, allItemsByNode,
isMenuOpen: false, isMenuOpen: false,
isConfiguring: false, isConfiguring: false,
attrs: { ...defaultWorkflowAttributes, ...data.attrs }
} }
console.debug("[layoutState] deserialize", data, state, defaultWorkflowAttributes) console.debug("[layoutState] deserialize", data, state, defaultWorkflowAttributes)

View File

@@ -1,8 +1,8 @@
import type { SerializedGraphCanvasState } from '$lib/ComfyGraphCanvas'; import type { SerializedGraphCanvasState } from '$lib/ComfyGraphCanvas';
import { clamp, type LGraphCanvas, type NodeID, type SerializedLGraph, type UUID } from '@litegraph-ts/core'; import { clamp, LGraphNode, type LGraphCanvas, type NodeID, type SerializedLGraph, type UUID, LGraph } from '@litegraph-ts/core';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store';
import type { SerializedLayoutState, WritableLayoutStateStore } from './layoutStates'; import { defaultWorkflowAttributes, type SerializedLayoutState, type WritableLayoutStateStore } from './layoutStates';
import ComfyGraph from '$lib/ComfyGraph'; import ComfyGraph from '$lib/ComfyGraph';
import layoutStates from './layoutStates'; import layoutStates from './layoutStates';
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
@@ -18,7 +18,8 @@ type ActiveCanvas = {
export type SerializedWorkflowState = { export type SerializedWorkflowState = {
graph: SerializedLGraph, graph: SerializedLGraph,
layout: SerializedLayoutState layout: SerializedLayoutState,
attrs: WorkflowAttributes
} }
/* /*
@@ -31,22 +32,45 @@ export type SerializedWorkflowState = {
*/ */
export type WorkflowInstID = UUID; export type WorkflowInstID = UUID;
/*
* Global workflow attributes
*/
export type WorkflowAttributes = {
/*
* Title of the workflow.
*/
title: string,
/*
* Name of the "Queue Prompt" button. Set to blank to hide the button.
*/
queuePromptButtonName: string,
/*
* If true, clicking the "Queue Prompt" button will run the default
* subgraph. Set this to false if you need special behavior before running
* any subgraphs, and instead use the `onDefaultQueueAction` event of the
* Comfy.QueueEvents node.
*/
queuePromptButtonRunWorkflow: boolean,
}
export class ComfyWorkflow { export class ComfyWorkflow {
/* /*
* Used for uniquely identifying the instance of the opened workflow in the frontend. * Used for uniquely identifying the instance of the opened workflow in the frontend.
*/ */
id: WorkflowInstID; id: WorkflowInstID;
/*
* Human-readable name on the tab
*/
title: string;
/* /*
* Graph of this workflow, whose nodes are bound to the UI layout * Graph of this workflow, whose nodes are bound to the UI layout
*/ */
graph: ComfyGraph; graph: ComfyGraph;
/*
* Global workflow attributes
*/
attrs: WorkflowAttributes
get layout(): WritableLayoutStateStore | null { get layout(): WritableLayoutStateStore | null {
return layoutStates.getLayout(this.id) return layoutStates.getLayout(this.id)
} }
@@ -58,7 +82,10 @@ export class ComfyWorkflow {
constructor(title: string) { constructor(title: string) {
this.id = uuidv4(); this.id = uuidv4();
this.title = title; this.attrs = {
...defaultWorkflowAttributes,
title,
}
this.graph = new ComfyGraph(this.id); this.graph = new ComfyGraph(this.id);
} }
@@ -115,7 +142,8 @@ export class ComfyWorkflow {
return { return {
graph: serializedGraph, graph: serializedGraph,
layout: serializedLayout layout: serializedLayout,
attrs: this.attrs
} }
} }
@@ -147,6 +175,8 @@ export class ComfyWorkflow {
// this.#invokeExtensions("loadedGraphNode", node); // this.#invokeExtensions("loadedGraphNode", node);
} }
this.attrs = data.attrs;
// Now restore the layout // Now restore the layout
// Subsequent added nodes will add the UI data to layoutState // Subsequent added nodes will add the UI data to layoutState
// TODO // TODO
@@ -163,6 +193,8 @@ export type WorkflowState = {
type WorkflowStateOps = { type WorkflowStateOps = {
getWorkflow: (id: WorkflowInstID) => ComfyWorkflow | null getWorkflow: (id: WorkflowInstID) => ComfyWorkflow | null
getWorkflowByGraph: (graph: LGraph) => ComfyWorkflow | null
getWorkflowByNode: (node: LGraphNode) => ComfyWorkflow | null
getWorkflowByNodeID: (id: NodeID) => ComfyWorkflow | null getWorkflowByNodeID: (id: NodeID) => ComfyWorkflow | null
getActiveWorkflow: () => ComfyWorkflow | null getActiveWorkflow: () => ComfyWorkflow | null
createNewWorkflow: (canvas: ComfyGraphCanvas, title?: string, setActive?: boolean) => ComfyWorkflow, createNewWorkflow: (canvas: ComfyGraphCanvas, title?: string, setActive?: boolean) => ComfyWorkflow,
@@ -185,6 +217,16 @@ function getWorkflow(id: WorkflowInstID): ComfyWorkflow | null {
return get(store).openedWorkflowsByID[id]; return get(store).openedWorkflowsByID[id];
} }
function getWorkflowByGraph(graph: LGraph): ComfyWorkflow | null {
if ("workflowID" in graph && graph.workflowID != null)
return getWorkflow((graph as ComfyGraph).workflowID);
return null;
}
function getWorkflowByNode(node: LGraphNode): ComfyWorkflow | null {
return getWorkflowByGraph(node.graph);
}
function getWorkflowByNodeID(id: NodeID): ComfyWorkflow | null { function getWorkflowByNodeID(id: NodeID): ComfyWorkflow | null {
return Object.values(get(store).openedWorkflows).find(w => { return Object.values(get(store).openedWorkflows).find(w => {
return w.graph.getNodeByIdRecursive(id) != null return w.graph.getNodeByIdRecursive(id) != null
@@ -216,8 +258,8 @@ function createNewWorkflow(canvas: ComfyGraphCanvas, title: string = "New Workfl
} }
function openWorkflow(canvas: ComfyGraphCanvas, data: SerializedAppState): ComfyWorkflow { function openWorkflow(canvas: ComfyGraphCanvas, data: SerializedAppState): ComfyWorkflow {
const [workflow, layoutState] = ComfyWorkflow.create(data.workflowName || "Workflow") const [workflow, layoutState] = ComfyWorkflow.create("Workflow")
workflow.deserialize(layoutState, { graph: data.workflow, layout: data.layout }) workflow.deserialize(layoutState, { graph: data.workflow, layout: data.layout, attrs: data.attrs })
const state = get(store); const state = get(store);
state.openedWorkflows.push(workflow); state.openedWorkflows.push(workflow);
@@ -285,6 +327,8 @@ const workflowStateStore: WritableWorkflowStateStore =
{ {
...store, ...store,
getWorkflow, getWorkflow,
getWorkflowByGraph,
getWorkflowByNode,
getWorkflowByNodeID, getWorkflowByNodeID,
getActiveWorkflow, getActiveWorkflow,
createNewWorkflow, createNewWorkflow,

View File

@@ -1,7 +1,7 @@
import { type WidgetLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates"; import { type WidgetLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
import selectionState from "$lib/stores/selectionState"; import selectionState from "$lib/stores/selectionState";
import type { FileData as GradioFileData } from "@gradio/upload"; import type { FileData as GradioFileData } from "@gradio/upload";
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID } from "@litegraph-ts/core"; import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID } from "@litegraph-ts/core";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { ComfyNodeID } from "./api"; import type { ComfyNodeID } from "./api";
import { type SerializedPrompt } from "./components/ComfyApp"; import { type SerializedPrompt } from "./components/ComfyApp";
@@ -176,23 +176,33 @@ export function workflowToGraphVis(workflow: SerializedLGraph): string {
export function promptToGraphVis(prompt: SerializedPrompt): string { export function promptToGraphVis(prompt: SerializedPrompt): string {
let out = "digraph {\n" let out = "digraph {\n"
const ids: Record<NodeID, number> = {}
let nextID = 0;
for (const pair of Object.entries(prompt.output)) { for (const pair of Object.entries(prompt.output)) {
const [id, o] = pair; const [id, o] = pair;
const outNode = prompt.workflow.nodes.find(n => n.id == id) if (ids[id] == null)
if (outNode) { ids[id] = nextID++;
if ("class_type" in o) {
for (const pair2 of Object.entries(o.inputs)) { for (const pair2 of Object.entries(o.inputs)) {
const [inpName, i] = pair2; const [inpName, i] = pair2;
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") { if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
// Link // Link
const inpNode = prompt.workflow.nodes.find(n => n.id == i[0]) const [inpID, inpSlot] = i;
if (ids[inpID] == null)
ids[inpID] = nextID++;
const inpNode = prompt.output[inpID]
if (inpNode) { if (inpNode) {
out += `"${inpNode.title}" -> "${outNode.title}"\n` out += `"${ids[inpID]}_${inpNode.class_type}" -> "${ids[id]}_${o.class_type}"\n`
} }
} }
else { else {
const value = String(i).substring(0, 20)
// Value // Value
out += `"${id}-${inpName}-${i}" -> "${outNode.title}"\n` out += `"${ids[id]}-${inpName}-${value}" -> "${ids[id]}_${o.class_type}"\n`
} }
} }
} }

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import workflowState from "$lib/stores/workflowState"; import workflowState, { ComfyWorkflow } from "$lib/stores/workflowState";
import { getNodeInfo } from "$lib/utils" import { getNodeInfo } from "$lib/utils"
import { Link, Toolbar } from "framework7-svelte" import { Link, Toolbar } from "framework7-svelte"
@@ -14,8 +14,9 @@
export let app: ComfyApp = undefined; export let app: ComfyApp = undefined;
let layoutState: WritableLayoutStateStore = null; let layoutState: WritableLayoutStateStore = null;
let fileInput: HTMLInputElement = undefined; let fileInput: HTMLInputElement = undefined;
let workflow: ComfyWorkflow | null = null;
$: layoutState = $workflowState.activeWorkflow?.layout; $: workflow = $workflowState.activeWorkflow;
function queuePrompt() { function queuePrompt() {
navigator.vibrate(20) navigator.vibrate(20)
@@ -72,9 +73,9 @@
{/if} {/if}
</div> </div>
<Toolbar bottom> <Toolbar bottom>
{#if $layoutState.attrs.queuePromptButtonName != ""} {#if workflow != null && workflow.attrs.queuePromptButtonName != ""}
<Link on:click={queuePrompt}> <Link on:click={queuePrompt}>
{$layoutState.attrs.queuePromptButtonName} {workflow.attrs.queuePromptButtonName}
</Link> </Link>
{/if} {/if}
<Link on:click={refreshCombos}>🔄</Link> <Link on:click={refreshCombos}>🔄</Link>

View File

@@ -10,7 +10,7 @@ import { graphToGraphVis } from "$lib/utils";
import { ComfyNumberNode } from "$lib/nodes/widgets"; import { ComfyNumberNode } from "$lib/nodes/widgets";
import { get } from "svelte/store"; import { get } from "svelte/store";
import layoutStates from "$lib/stores/layoutStates"; import layoutStates from "$lib/stores/layoutStates";
import { ComfyWorkflow } from "$lib/components/ComfyApp"; import { ComfyWorkflow } from "$lib/stores/workflowState";
export default class ComfyGraphTests extends UnitTest { export default class ComfyGraphTests extends UnitTest {
test__onNodeAdded__updatesLayoutState() { test__onNodeAdded__updatesLayoutState() {

View File

@@ -1,4 +1,4 @@
import { LGraph, LiteGraph, Subgraph, type SlotLayout } from "@litegraph-ts/core" import { LGraph, LiteGraph, Subgraph, type SlotLayout, NodeMode } from "@litegraph-ts/core"
import { Watch } from "@litegraph-ts/nodes-basic" import { Watch } from "@litegraph-ts/nodes-basic"
import { expect } from 'vitest' import { expect } from 'vitest'
import UnitTest from "./UnitTest" import UnitTest from "./UnitTest"
@@ -139,6 +139,39 @@ export default class ComfyPromptSerializerTests extends UnitTest {
expect(result.output[output.id].inputs).toEqual({}) expect(result.output[output.id].inputs).toEqual({})
} }
test__serialize__shouldIgnoreInactiveSubgraphs() {
const ser = new ComfyPromptSerializer();
const graph = new ComfyGraph();
const output = LiteGraph.createNode(MockBackendOutput)
const link = LiteGraph.createNode(MockBackendLink)
const input = LiteGraph.createNode(MockBackendInput)
const subgraph = LiteGraph.createNode(Subgraph)
const graphInput = subgraph.addGraphInput("testIn", "number")
const graphOutput = subgraph.addGraphOutput("testOut", "number")
graph.add(subgraph)
graph.add(output)
subgraph.subgraph.add(link)
graph.add(input)
output.connect(0, subgraph, 0)
graphInput.innerNode.connect(0, link, 0)
link.connect(0, graphOutput.innerNode, 0)
subgraph.connect(0, input, 0)
subgraph.mode = NodeMode.NEVER;
const result = ser.serialize(graph)
expect(Object.keys(result.output)).toHaveLength(3);
expect(result.output[input.id].inputs["in"]).toBeUndefined();
expect(result.output[link.id].inputs["in"]).toBeInstanceOf(Array)
expect(result.output[link.id].inputs["in"][0]).toEqual(output.id)
expect(result.output[output.id].inputs).toEqual({})
}
test__serialize__shouldFollowSubgraphsRecursively() { test__serialize__shouldFollowSubgraphsRecursively() {
const ser = new ComfyPromptSerializer(); const ser = new ComfyPromptSerializer();
const graph = new ComfyGraph(); const graph = new ComfyGraph();