Workflow properties

This commit is contained in:
space-nuko
2023-05-05 16:46:28 -05:00
parent 578e38e58b
commit 7ddda80cf6
14 changed files with 489 additions and 179 deletions

View File

@@ -6,7 +6,7 @@
import { BlockTitle } from "@gradio/atoms";
import ComfyUIPane from "./ComfyUIPane.svelte";
import ComfyApp, { type SerializedAppState } from "./ComfyApp";
import { Checkbox } from "@gradio/form"
import { Checkbox, TextBox } from "@gradio/form"
import uiState from "$lib/stores/uiState";
import layoutState from "$lib/stores/layoutState";
import { ImageViewer } from "$lib/ImageViewer";
@@ -29,6 +29,7 @@
let containerElem: HTMLDivElement;
let resizeTimeout: NodeJS.Timeout | null;
let hasShownUIHelpToast: boolean = false;
let uiTheme: string = "";
let debugLayout: boolean = false;
@@ -46,7 +47,10 @@
function queuePrompt() {
console.log("Queuing!");
app.queuePrompt(0, 1);
let subworkflow = $uiState.subWorkflow;
if (subworkflow === "")
subworkflow = null
app.queuePrompt(0, 1, subworkflow);
}
$: if (app?.lCanvas) app.lCanvas.allow_dragnodes = !$uiState.nodesLocked;
@@ -164,6 +168,12 @@
}
</script>
<svelte:head>
{#if uiTheme === "anapnoe"}
<link rel="stylesheet" href="/src/scss/ux.scss">
{/if}
</svelte:head>
<div id="main">
<div id="dropzone" class="dropzone"></div>
<div id="container" bind:this={containerElem}>
@@ -220,16 +230,24 @@
<Button variant="secondary" on:click={doRefreshCombos}>
🔄
</Button>
<Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/>
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
<TextBox bind:value={$uiState.subWorkflow} label="Subworkflow" show_label={true} lines={1} max_lines={1}/>
<label class="label" for="enable-ui-editing">
<BlockTitle>Enable UI Editing</BlockTitle>
<select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}>
<option value="disabled">Disabled</option>
<option value="widgets">Widgets</option>
</select>
</label>
<label class="label" for="ui-theme">
<BlockTitle>Theme</BlockTitle>
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
<option value="">None</option>
<option value="anapnoe">Anapnoe</option>
</select>
</label>
<select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}>
<option value="disabled">Disabled</option>
<option value="widgets">Widgets</option>
</select>
</div>
<LightboxModal />
</div>

View File

@@ -25,6 +25,7 @@ import ComfyGraph from "$lib/ComfyGraph";
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import { get } from "svelte/store";
import uiState from "$lib/stores/uiState";
import { promptToGraphVis, toGraphVis } from "$lib/utils";
export const COMFYBOX_SERIAL_VERSION = 1;
@@ -54,9 +55,11 @@ export type SerializedPromptInputs = {
class_type: string
}
export type SerializedPromptOutput = Record<string, SerializedPromptInputs>
export type SerializedPrompt = {
workflow: SerializedLGraph,
output: Record<string, SerializedPromptInputs>
output: SerializedPromptOutput
}
export type Progress = {
@@ -420,7 +423,7 @@ export default class ComfyApp {
* Converts the current graph workflow for sending to the API
* @returns The workflow and node links
*/
async graphToPrompt(): Promise<SerializedPrompt> {
async graphToPrompt(tag: string | null = null): Promise<SerializedPrompt> {
// Run frontend-only logic
this.lGraph.runStep(1)
@@ -438,6 +441,11 @@ export default class ComfyApp {
const node = node_ as ComfyBackendNode;
if (tag && node.tags.indexOf(tag) === -1) {
console.debug("Skipping tagged node", tag, node.tags)
continue;
}
if (node.mode === NodeMode.NEVER) {
// Don't serialize muted nodes
continue;
@@ -451,6 +459,11 @@ export default class ComfyApp {
const inp = node.inputs[i];
const inputLink = node.getInputLink(i)
const inputNode = node.getInputNode(i)
if (inputNode && tag && "tags" in inputNode && (inputNode.tags as string[]).indexOf(tag) === -1) {
continue;
}
if (!inputLink || !inputNode) {
if ("config" in inp) {
const defaultValue = (inp as IComfyInputSlot).config?.defaultValue
@@ -489,17 +502,36 @@ export default class ComfyApp {
if (parent) {
const seen = {}
let link = node.getInputLink(i);
while (parent && !parent.isBackendNode) {
const isValidParent = (parent: ComfyGraphNode) => {
if (!parent || parent.isBackendNode)
return false;
if ("tags" in parent && (parent.tags as string[]).indexOf(tag) === -1)
return false;
return true;
}
while (isValidParent(parent)) {
link = parent.getInputLink(link.origin_slot);
if (link && !seen[link.id]) {
seen[link.id] = true
parent = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
const inputNode = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
if (inputNode && "tags" in inputNode && tag && (inputNode.tags as string[]).indexOf(tag) === -1) {
console.debug("Skipping tagged parent node", tag, node.tags)
parent = null;
}
else {
parent = inputNode;
}
} else {
parent = null;
}
}
if (link && parent && parent.isBackendNode) {
if ("tags" in parent && tag && (parent.tags as string[]).indexOf(tag) === -1)
continue;
const input = node.inputs[i]
// TODO can null be a legitimate value in some cases?
// Nodes like CLIPLoader will never have a value in the frontend, hence "null".
@@ -522,15 +554,19 @@ export default class ComfyApp {
if (Array.isArray(output[o].inputs[i])
&& output[o].inputs[i].length === 2
&& !output[output[o].inputs[i][0]]) {
console.debug("Prune removed node link", o, i, output[o].inputs[i])
delete output[o].inputs[i];
}
}
}
console.warn({ workflow, output })
console.warn(promptToGraphVis({ workflow, output }))
return { workflow, output };
}
async queuePrompt(num: number, batchCount: number = 1) {
async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) {
this.queueItems.push({ num, batchCount });
// Only have one action process the items so each one gets a unique seed correctly
@@ -545,7 +581,7 @@ export default class ComfyApp {
console.log(`Queue get! ${num} ${batchCount}`);
for (let i = 0; i < batchCount; i++) {
const p = await this.graphToPrompt();
const p = await this.graphToPrompt(tag);
try {
await this.api.queuePrompt(num, p);
@@ -626,12 +662,8 @@ export default class ComfyApp {
if ("config" in input) {
const comfyInput = input as IComfyInputSlot;
console.warn("RefreshCombo", comfyInput.defaultWidgetNode, comfyInput)
if (comfyInput.defaultWidgetNode == nodes.ComfyComboNode && def["input"]["required"][comfyInput.name] !== undefined) {
comfyInput.config.values = def["input"]["required"][comfyInput.name][0];
console.warn("RefreshCombo", comfyInput.config.values, def["input"]["required"][comfyInput.name])
const inputNode = node.getInputNode(index)
if (inputNode && "doAutoConfig" in inputNode) {

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { BlockTitle } from "@gradio/atoms";
import { createEventDispatcher } from "svelte";
export let value: string = "";
export let values: string[] = [""];
export let name: string = "";
let value_: string = ""
$: handleChange(value);
const dispatch = createEventDispatcher<{
change: string;
submit: undefined;
blur: undefined;
}>();
function handleChange(val: string) {
if (val != value_)
dispatch("change", val);
value_ = val
}
</script>
<label class="select-wrapper">
<BlockTitle>{name}</BlockTitle>
<div class="select">
<select on:blur bind:value>
{#each values as value}
<option {value}>
{value}
</option>
{/each}
</select>
</div>
</label>
<style lang="scss">
.select-wrapper {
width: 100%;
.select {
width: 100%;
select {
width: 100%
}
}
}
.select-title {
padding: 0.2rem;
}
</style>

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import { BlockTitle } from "@gradio/atoms";
import { createEventDispatcher } from "svelte";
export let value: number = 0;
export let step: number = 1;
export let name: string = "";
let value_: number = 0;
$: value;
$: handleChange(value);
const dispatch = createEventDispatcher<{
change: number;
submit: undefined;
blur: undefined;
}>();
function handleChange(val: number) {
if (val != value_)
dispatch("change", val);
value_ = val
}
</script>
<label class="number-wrapper">
<BlockTitle>{name}</BlockTitle>
<div class="number">
<input type="number" bind:value {step}>
</div>
</label>
<style lang="scss">
.number-wrapper {
width: 100%;
.number {
width: 100%;
input {
width: 100%
}
}
}
</style>

View File

@@ -2,10 +2,11 @@
import { Block, BlockTitle } from "@gradio/atoms";
import { TextBox, Checkbox } from "@gradio/form";
import { LGraphNode } from "@litegraph-ts/core"
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES } from "$lib/stores/layoutState"
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
import { get } from "svelte/store"
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import type { ComfyWidgetNode } from "$lib/nodes";
import type { ComfyWidgetNode } from "$lib/nodes";
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
import ComfyComboProperty from "./ComfyComboProperty.svelte";
let target: IDragItem | null = null;
let node: LGraphNode | null = null;
@@ -20,6 +21,10 @@
node = null;
}
}
else if ($layoutState.currentSelectionNodes.length > 0) {
target = null;
node = $layoutState.currentSelectionNodes[0]
}
else {
target = null
node = null;
@@ -29,14 +34,14 @@
$: {
if (node != null)
targetType = node.type || "Widget"
targetType = node.type || "Node"
else if (target)
targetType = "group"
targetType = "Group"
else
targetType = "???"
targetType = ""
}
function updateAttribute(entry: any, value: any) {
function updateAttribute(entry: AttributesSpec, value: any) {
if (target) {
const name = entry.name
console.warn("updateAttribute", name, value)
@@ -51,7 +56,7 @@
}
}
function updateProperty(entry: any, value: any) {
function updateProperty(entry: AttributesSpec, value: any) {
if (node) {
const name = entry.name
console.warn("updateProperty", name, value)
@@ -64,125 +69,195 @@
}
}
}
function getVar(node: LGraphNode, entry: AttributesSpec) {
let value = node[entry.name]
if (entry.serialize)
value = entry.serialize(value)
console.debug("[ComfyProperties] getVar", entry, value, node)
return value
}
function updateVar(entry: any, value: any) {
if (node) {
const name = entry.name
console.warn("updateProperty", name, value)
if (entry.deserialize)
value = entry.deserialize(value)
console.debug("[ComfyProperties] updateVar", entry, value, name, node)
node[name] = value;
if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
}
}
function updateWorkflowAttribute(entry: AttributesSpec, value: any) {
const name = entry.name
console.warn("updateWorkflowAttribute", name, value)
$layoutState.attrs[name] = value
$layoutState = $layoutState
}
</script>
<div class="props">
{#if target}
<div class="top">
<div class="target-name">
<span>
<span class="title">{target.attrs.title}</span>
<div class="top">
<div class="target-name">
<span>
<span class="title">{target?.attrs?.title || node?.title || "Workflow"}<span>
{#if targetType !== ""}
<span class="type">({targetType})</span>
{/if}
</span>
</div>
</div>
<div class="props-entries">
{#each ALL_ATTRIBUTES as category(category.categoryName)}
<div class="category-name">
<span>
<span class="title">{category.categoryName}</span>
</span>
</div>
</div>
<div class="props-entries">
{#each ALL_ATTRIBUTES as category(category.categoryName)}
<div class="category-name">
<span>
<span class="title">{category.categoryName}</span>
</span>
</div>
{#each category.specs as spec(spec.name)}
{#if spec.location === "widget" && spec.name in target.attrs}
{#each category.specs as spec(spec.name)}
{#if spec.location === "widget" && target && spec.name in target.attrs}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={target.attrs[spec.name]}
on:change={(e) => updateAttribute(spec, e.detail)}
on:input={(e) => updateAttribute(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={target.attrs[spec.name]}
on:change={(e) => updateAttribute(spec, e.detail)}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={target.attrs[spec.name]}
step={1}
on:change={(e) => updateAttribute(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={target.attrs[spec.name]}
values={spec.values}
on:changed={(e) => updateAttribute(spec, e.detail)}
/>
{/if}
</div>
{:else if node}
{#if spec.location === "nodeProps" && spec.name in node.properties}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={target.attrs[spec.name]}
on:change={(e) => updateAttribute(spec, e.detail)}
on:input={(e) => updateAttribute(spec, e.detail)}
value={node.properties[spec.name]}
on:change={(e) => updateProperty(spec, e.detail)}
on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={target.attrs[spec.name]}
on:change={(e) => updateAttribute(spec, e.detail)}
value={node.properties[spec.name]}
label={spec.name}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={node.properties[spec.name]}
step={1}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={node.properties[spec.name]}
values={spec.values}
on:changed={(e) => updateProperty(spec, e.detail)}
/>
{/if}
</div>
{:else if spec.location === "nodeVars" && spec.name in node}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
on:input={(e) => updateVar(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
label={spec.name}
/>
{:else if spec.type === "number"}
<label class="number-wrapper">
<BlockTitle>{spec.name}</BlockTitle>
<div class="number">
<input
type="number"
value={target.attrs[spec.name]}
step={1}
on:change={(e) => updateAttribute(spec, e.currentTarget.valueAsNumber)}
on:input={(e) => updateAttribute(spec, e.currentTarget.valueAsNumber)}
/>
</div>
</label>
<ComfyNumberProperty
name={spec.name}
value={getVar(node, spec)}
step={1}
on:change={(e) => updateVar(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<label class="select-wrapper">
<BlockTitle>{spec.name}</BlockTitle>
<div class="select">
<select
value={target.attrs[spec.name]}
on:change={(e) => updateAttribute(spec, e.currentTarget.options[e.currentTarget.selectedIndex].value)}>
{#each spec.values as value}
<option value={value}>
{value}
</option>
{/each}
</select>
</div>
</label>
<ComfyComboProperty
name={spec.name}
value={getVar(node, spec)}
values={spec.values}
on:changed={(e) => updateVar(spec, e.detail)}
/>
{/if}
</div>
{:else if node}
{#if spec.location === "nodeProps" && spec.name in node.properties}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={node.properties[spec.name]}
on:change={(e) => updateProperty(spec, e.detail)}
on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={node.properties[spec.name]}
on:change={(e) => updateProperty(spec, e.detail)}
label={spec.name}
/>
{:else if spec.type === "number"}
<label class="number-wrapper">
<BlockTitle>{spec.name}</BlockTitle>
<div class="number">
<input
type="number"
value={node.properties[spec.name]}
step={1}
on:change={(e) => updateProperty(spec, e.currentTarget.valueAsNumber)}
on:input={(e) => updateProperty(spec, e.currentTarget.valueAsNumber)}
/>
</div>
</label>
{:else if spec.type === "enum"}
<label class="select-wrapper">
<BlockTitle>{spec.name}</BlockTitle>
<div class="select">
<select
value={node.properties[spec.name]}
on:change={(e) => updateProperty(spec, e.currentTarget.options[e.currentTarget.selectedIndex].value)}>
{#each spec.values as value}
<option value={value}>
{value}
</option>
{/each}
</select>
</div>
</label>
{/if}
</div>
{/if}
{/if}
{/each}
{:else if spec.location === "workflow" && spec.name in $layoutState.attrs}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={$layoutState.attrs[spec.name]}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={$layoutState.attrs[spec.name]}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={$layoutState.attrs[spec.name]}
step={1}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={$layoutState.attrs[spec.name]}
values={spec.values}
on:changed={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{/if}
</div>
{/if}
{/each}
</div>
{/if}
{/each}
</div>
</div>
<style lang="scss">
@@ -218,34 +293,6 @@
}
}
.number-wrapper {
width: 100%;
.number {
width: 100%;
input {
width: 100%
}
}
}
.select-wrapper {
width: 100%;
.select {
width: 100%;
select {
width: 100%
}
}
}
.select-title {
padding: 0.2rem;
}
.bottom {
/* width: 100%;
height: auto;