Start restore parameters

This commit is contained in:
space-nuko
2023-05-29 00:16:02 -05:00
parent fde480cb43
commit 26ab7989c8
10 changed files with 485 additions and 110 deletions

View File

@@ -224,13 +224,13 @@ const Metadata = z.object({
extra_data: ExtraData extra_data: ExtraData
}) })
const ComfyBoxStdPrompt = z.object({ const StdPrompt = z.object({
version: z.number(), version: z.number(),
metadata: Metadata, metadata: Metadata,
parameters: Parameters parameters: Parameters
}) })
export default ComfyBoxStdPrompt export default StdPrompt
/* /*
* A standardized Stable Diffusion parameter format that should be used with an * A standardized Stable Diffusion parameter format that should be used with an
@@ -260,4 +260,4 @@ export default ComfyBoxStdPrompt
* "see" width 1024 and height 1024, even though the only parameter exposed from * "see" width 1024 and height 1024, even though the only parameter exposed from
* the frontend was the scale of 2.) * the frontend was the scale of 2.)
*/ */
export type ComfyBoxStdPrompt = z.infer<typeof ComfyBoxStdPrompt> export type ComfyBoxStdPrompt = z.infer<typeof StdPrompt>

View File

@@ -1,31 +1,88 @@
import type { ComfyBoxStdGroupLoRA, ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt"; import type { ComfyBoxStdGroupLoRA, ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
import type { SerializedPrompt, SerializedPromptInputs } from "./components/ComfyApp"; import StdPrompt from "$lib/ComfyBoxStdPrompt";
import type { SafeParseReturnType, ZodError } from "zod";
import type { ComfyNodeID } from "./api";
import type { SerializedAppState, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll } from "./components/ComfyApp";
import { ComfyComboNode, type ComfyWidgetNode } from "./nodes/widgets";
import { basename, isSerializedPromptInputLink } from "./utils";
export type ComfyPromptConverter = (stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs, nodeID: ComfyNodeID) => void; export type ComfyPromptConverter = {
encoder: ComfyPromptEncoder,
function LoraLoader(stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs) { decoder: ComfyPromptDecoder
const params = stdPrompt.parameters
const lora: ComfyBoxStdGroupLoRA = {
model_name: inputs["lora_name"],
strength_unet: inputs["strength_model"],
strength_tenc: inputs["strength_clip"]
}
if (params.lora)
params.lora.push(lora)
else
params.lora = [lora]
} }
const ALL_CONVERTERS: Record<string, ComfyPromptConverter> = { //
LoraLoader export type ComfyDecodeArgument = {
groupName: string,
keyName: string,
value: any,
widgetNode: ComfyWidgetNode
};
export type ComfyPromptEncoder = (stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs, nodeID: ComfyNodeID) => void;
export type ComfyPromptDecoder = (args: ComfyDecodeArgument[]) => void;
const LoraLoader: ComfyPromptConverter = {
encoder: (stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs) => {
const params = stdPrompt.parameters
const loras: ComfyBoxStdGroupLoRA[] = params.lora
for (const lora of loras) {
lora.model_hashes = {
addnet_shorthash: null // TODO find hashes for model!
}
}
},
decoder: (args: ComfyDecodeArgument[]) => {
// Find corresponding model names in the ComfyUI models folder from the model base filename
for (const arg of args) {
if (arg.groupName === "lora" && arg.keyName === "model_name" && arg.widgetNode.is(ComfyComboNode)) {
const modelBasename = basename(arg.value);
const found = arg.widgetNode.properties.values.find(k => k.indexOf(modelBasename) !== -1)
if (found)
arg.value = found;
}
}
}
}
// input name -> group/key in standard prompt
type ComfyStdPromptMapping = Record<string, string>
type ComfyStdPromptSpec = {
paramMapping: ComfyStdPromptMapping,
extraParams?: Record<string, string>,
converter?: ComfyPromptConverter,
}
const ALL_SPECS: Record<string, ComfyStdPromptSpec> = {
"KSampler": {
paramMapping: {
cfg: "k_sampler.cfg_scale",
seed: "k_sampler.seed",
steps: "k_sampler.steps",
sampler_name: "k_sampler.sampler_name",
scheduler: "k_sampler.scheduler",
denoise: "k_sampler.denoise",
},
},
"LoraLoader": {
paramMapping: {
lora_name: "lora.model_name",
strength_model: "lora.strength_unet",
strength_clip: "lora.strength_tenc",
},
extraParams: {
"lora.module_name": "LoRA",
},
converter: LoraLoader,
}
} }
const COMMIT_HASH: string = __GIT_COMMIT_HASH__; const COMMIT_HASH: string = __GIT_COMMIT_HASH__;
export default class ComfyBoxStdPromptSerializer { export default class ComfyBoxStdPromptSerializer {
serialize(prompt: SerializedPrompt): ComfyBoxStdPrompt { serialize(prompt: SerializedPromptInputsAll, workflow?: SerializedAppState): [SafeParseReturnType<any, ComfyBoxStdPrompt>, any] {
const stdPrompt: ComfyBoxStdPrompt = { const stdPrompt: ComfyBoxStdPrompt = {
version: 1, version: 1,
metadata: { metadata: {
@@ -33,23 +90,57 @@ export default class ComfyBoxStdPromptSerializer {
commit_hash: COMMIT_HASH, commit_hash: COMMIT_HASH,
extra_data: { extra_data: {
comfybox: { comfybox: {
workflows: [] // TODO!!!
} }
} }
}, },
parameters: {} parameters: {}
} }
for (const [nodeID, inputs] of Object.entries(prompt.output)) { for (const [nodeID, inputs] of Object.entries(prompt)) {
const classType = inputs.class_type const classType = inputs.class_type
const converter = ALL_CONVERTERS[classType] const spec = ALL_SPECS[classType]
if (converter) { if (spec) {
converter(stdPrompt, inputs.inputs, nodeID) console.warn("SPEC", spec, inputs)
let targets = {}
for (const [comfyKey, stdPromptKey] of Object.entries(spec.paramMapping)) {
const inputValue = inputs.inputs[comfyKey];
if (inputValue != null && !isSerializedPromptInputLink(inputValue)) {
console.warn("GET", comfyKey, inputValue)
const trail = stdPromptKey.split(".");
let target = null;
console.warn(trail, trail.length - 2);
for (let index = 0; index < trail.length - 1; index++) {
const name = trail[index];
if (index === 0) {
targets[name] ||= {}
target = targets[name]
} }
else { else {
console.warn("No StdPrompt type converter for comfy class!", classType) target = target[name]
}
console.warn(index, name, target)
}
let name = trail[trail.length - 1]
target[name] = inputValue
console.warn(stdPrompt.parameters)
} }
} }
return stdPrompt // TODO converter.encode
for (const [groupName, group] of Object.entries(targets)) {
stdPrompt.parameters[groupName] ||= []
stdPrompt.parameters[groupName].push(group)
}
}
else {
console.warn("No StdPrompt type spec for comfy class!", classType)
}
}
return [StdPrompt.safeParse(stdPrompt), stdPrompt];
} }
} }

View File

@@ -1064,9 +1064,6 @@ export default class ComfyApp {
// console.debug(graphToGraphVis(workflow.graph)) // console.debug(graphToGraphVis(workflow.graph))
// console.debug(promptToGraphVis(p)) // console.debug(promptToGraphVis(p))
const stdPrompt = this.stdPromptSerializer.serialize(p);
// console.warn("STD", stdPrompt);
const extraData: ComfyBoxPromptExtraData = { const extraData: ComfyBoxPromptExtraData = {
extra_pnginfo: { extra_pnginfo: {
comfyBoxWorkflow: wf, comfyBoxWorkflow: wf,

View File

@@ -32,6 +32,7 @@
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte"; import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte"; import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte";
import uiQueueState from "$lib/stores/uiQueueState"; import uiQueueState from "$lib/stores/uiQueueState";
import type { SerializedAppState, SerializedPromptInputsAll } from "./ComfyApp";
export let app: ComfyApp; export let app: ComfyApp;
@@ -124,19 +125,22 @@
let showModal = false; let showModal = false;
let expandAll = false; let expandAll = false;
let selectedPrompt = null; let selectedPrompt: SerializedPromptInputsAll | null = null;
let selectedWorkflow: SerializedAppState | null = null;
let selectedImages = []; let selectedImages = [];
function showPrompt(entry: QueueUIEntry) { function showPrompt(entry: QueueUIEntry) {
if (entry.error != null) { if (entry.error != null) {
showModal = false; showModal = false;
expandAll = false; expandAll = false;
selectedPrompt = null; selectedPrompt = null;
selectedWorkflow = null;
selectedImages = []; selectedImages = [];
showError(entry.entry.promptID); showError(entry.entry.promptID);
} }
else { else {
selectedPrompt = entry.entry.prompt; selectedPrompt = entry.entry.prompt,
selectedWorkflow = entry.entry.extraData.extra_pnginfo.comfyBoxWorkflow
selectedImages = entry.images; selectedImages = entry.images;
showModal = true; showModal = true;
expandAll = false expandAll = false
@@ -145,6 +149,7 @@
function closeModal() { function closeModal() {
selectedPrompt = null selectedPrompt = null
selectedWorkflow = null;
selectedImages = [] selectedImages = []
showModal = false; showModal = false;
expandAll = false; expandAll = false;
@@ -165,7 +170,7 @@
</div> </div>
<svelte:fragment let:closeDialog> <svelte:fragment let:closeDialog>
{#if selectedPrompt} {#if selectedPrompt}
<PromptDisplay closeModal={() => { closeModal(); closeDialog(); }} {app} prompt={selectedPrompt} images={selectedImages} {expandAll} /> <PromptDisplay closeModal={() => { closeModal(); closeDialog(); }} {app} prompt={selectedPrompt} workflow={selectedWorkflow} images={selectedImages} {expandAll} />
{/if} {/if}
</svelte:fragment> </svelte:fragment>
<div slot="buttons" let:closeDialog> <div slot="buttons" let:closeDialog>

View File

@@ -173,7 +173,7 @@
} }
.comfy-settings-entries { .comfy-settings-entries {
padding: 3rem 3rem; padding: 2rem 0.75rem;
height: 100%; height: 100%;
} }

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { TextBox } from "@gradio/form"; import { TextBox } from "@gradio/form";
import type { SerializedPromptInput, SerializedPromptInputsAll } from "./ComfyApp"; import type { SerializedAppState, SerializedPrompt, SerializedPromptInput, SerializedPromptInputsAll } from "./ComfyApp";
import { Block, BlockLabel, BlockTitle } from "@gradio/atoms"; import { Block, BlockLabel, BlockTitle } from "@gradio/atoms";
import { JSON as JSONComponent } from "@gradio/json"; import { JSON as JSONComponent } from "@gradio/json";
import { JSON as JSONIcon, Copy, Check } from "@gradio/icons"; import { JSON as JSONIcon, Copy, Check } from "@gradio/icons";
@@ -13,16 +13,39 @@
import workflowState, { type ComfyBoxWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState"; import workflowState, { type ComfyBoxWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions"; import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
import type ComfyApp from "./ComfyApp"; import type ComfyApp from "./ComfyApp";
import { TabItem, Tabs } from "@gradio/tabs";
import { type ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
import JsonView from "./JsonView.svelte";
import type { ZodError } from "zod";
const splitLength = 50; const splitLength = 50;
export let prompt: SerializedPromptInputsAll; export let prompt: SerializedPromptInputsAll;
export let workflow: SerializedAppState | null;
export let images: string[] = []; // list of image URLs to ComfyUI's /view? endpoint export let images: string[] = []; // list of image URLs to ComfyUI's /view? endpoint
export let isMobile: boolean = false; export let isMobile: boolean = false;
export let expandAll: boolean = false; export let expandAll: boolean = false;
export let closeModal: () => void; export let closeModal: () => void;
export let app: ComfyApp; export let app: ComfyApp;
let stdPrompt: ComfyBoxStdPrompt | null;
let stdPromptError: ZodError<any> | null;
$: {
const [result, orig] = new ComfyBoxStdPromptSerializer().serialize(prompt, workflow);
if (result.success === true) {
stdPrompt = result.data;
stdPromptError = null;
}
else {
stdPrompt = orig;
stdPromptError = result.error;
}
}
let selectedTab: "restore-parameters" | "send-outputs" | "standard-prompt" | "prompt" = "standard-prompt";
let selected_image: number | null = null; let selected_image: number | null = null;
let galleryStyle: Styles = { let galleryStyle: Styles = {
@@ -127,8 +150,58 @@
<div class="prompt-display"> <div class="prompt-display">
<div class="prompt-and-sends"> <div class="prompt-and-sends">
<Tabs bind:selected={selectedTab}>
<TabItem id="restore-parameters" name="Restore Parameters">
<Block>
<BlockTitle>Parameters</BlockTitle>
</Block>
</TabItem>
{#if comfyBoxImages.length > 0}
<TabItem id="send-outputs" name="Send Outputs">
<Block>
<BlockTitle>Output type: {litegraphType}</BlockTitle>
{#if receiveTargets.length > 0}
<ReceiveOutputTargets {receiveTargets} on:select={(e) => sendOutput(e.detail.workflow, e.detail.targetNode)} />
{:else}
<div class="outputs-message">No receive output targets found across all workflows.</div>
{/if}
</Block>
</TabItem>
{/if}
<TabItem id="standard-prompt" name="Standard Prompt">
{#if stdPromptError}
<Block>
<BlockTitle><div style:color="#F88">Parsing Error</div></BlockTitle>
<div class="scroll-container">
<div class="json">
<JsonView json={stdPromptError} />
</div>
</div>
</Block>
<Block>
<BlockTitle><div>Original Data</div></BlockTitle>
<div class="scroll-container">
<div class="json">
<JsonView json={stdPrompt} />
</div>
</div>
</Block>
{:else if stdPrompt}
<Block>
<div class="scroll-container">
<div class="json">
<JsonView json={stdPrompt} />
</div>
</div>
</Block>
{:else}
<Block>
(No standard prompt)
</Block>
{/if}
</TabItem>
<TabItem id="prompt" name="Prompt">
<Block> <Block>
<Accordion label="Prompt" open={expandAll || comfyBoxImages.length === 0}>
<div class="scroll-container"> <div class="scroll-container">
<Block> <Block>
{#each Object.entries(prompt) as [nodeID, inputs], i} {#each Object.entries(prompt) as [nodeID, inputs], i}
@@ -178,22 +251,9 @@
{/each} {/each}
</Block> </Block>
</div> </div>
</Accordion>
</Block> </Block>
{#if comfyBoxImages.length > 0} </TabItem>
<Block> </Tabs>
<Accordion label="Send Outputs To..." open={true}>
<Block>
<BlockTitle>Output type: {litegraphType}</BlockTitle>
{#if receiveTargets.length > 0}
<ReceiveOutputTargets {receiveTargets} on:select={(e) => sendOutput(e.detail.workflow, e.detail.targetNode)} />
{:else}
<div class="outputs-message">No receive output targets found across all workflows.</div>
{/if}
</Block>
</Accordion>
</Block>
{/if}
</div> </div>
{#if images.length > 0} {#if images.length > 0}
<div class="image-container"> <div class="image-container">
@@ -221,23 +281,36 @@
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
overflow-y: auto;
flex-direction: column; flex-direction: column;
@media (min-width: 1600px) { @media (min-width: 1200px) {
flex-direction: row; flex-direction: row;
} }
} }
.prompt-and-sends {
width: 50%;
.scroll-container { .scroll-container {
position: relative; position: relative;
/* overflow-y: auto; */ /* overflow-y: auto; */
flex: 1 1 0%; flex: 1 1 0%;
} }
.json {
@include json-view;
}
.prompt-and-sends {
width: 50%;
overflow-y: auto;
:global(>.tabs) {
height: 100%;
:global(>.tabitem) {
overflow-y: auto;
}
}
.copy-button { .copy-button {
display: flex; display: flex;
position: absolute; position: absolute;

View File

@@ -0,0 +1,205 @@
import type { INodeInputSlot, NodeID } from "@litegraph-ts/core";
import type { SerializedPrompt } from "./components/ComfyApp";
import type { ComfyWidgetNode } from "./nodes/widgets";
import type { SerializedComfyWidgetNode } from "./nodes/widgets/ComfyWidgetNode";
import { isComfyWidgetNode } from "./stores/layoutStates";
import type { ComfyBoxWorkflow } from "./stores/workflowState";
import { isSerializedPromptInputLink } from "./utils";
import ComfyBoxStdPromptSerializer from "./ComfyBoxStdPromptSerializer";
interface RestoreParamSource {
finalValue: any
}
/*
* A serialized ComfyWidgetNode from the saved workflow that corresponds
* *exactly* to a node with the same ID in the current workflow. Easiest case
* since the parameter value can just be copied without much fuss.
*/
interface RestoreParamSourceWorkflowNode extends RestoreParamSource {
sourceNode: SerializedComfyWidgetNode
}
/*
* A value received by the ComfyUI *backend* that corresponds to a value that
* was held in a ComfyWidgetNode. These may not necessarily be one-to-one
* because there can be extra frontend-only processing nodes between the two.
*
* (Example: a node that converts a random prompt template into a final prompt
* string, then passes *that* prompt string to the backend. The backend will not
* see the template string, so it will be missing in the arguments to ComfyUI's
* prompt endpoint. Hence this parameter source won't account for those kinds of
* values.)
*/
interface RestoreParamSourceBackendNodeInput extends RestoreParamSource {
backendNode: SerializedComfyWidgetNode,
/*
* If false, this node was connected to the backend node across one or more
* additional frontend nodes, so the value in the source may not correspond
* exactly to the widget's original value
*/
isDirectAttachment: boolean
}
/*
* A value contained in the standard prompt extracted from the saved workflow.
*/
interface RestoreParamSourceStdPrompt<T, K extends keyof T> extends RestoreParamSource {
/*
* Name of the group containing the value to pass
*
* "lora"
*/
groupName: string,
/*
* The standard prompt group containing the value and metadata like
* "positive"/"negative" for identification use
*
* { "$meta": { ... }, model_name: "...", model_hashes: [...], ... }
*/
group: T,
/*
* Key of the group parameter holding the actual value
* "model_name"
*/
key: K,
/*
* The raw value as saved to the prompt, not accounting for stuff like hashes
*
* "contrastFix"
*/
rawValue: T[K]
/*
* The *actual* value that will be copied into the ComfyWidgetNode, after
* conversion to account for filepaths/etc. from prompt adapters has been
* completed
*
* "models/lora/contrastFix.safetensors"
*/
finalValue: any
}
export type RestoreParamTarget = {
/*
* Node that will receive the parameter from the prompt
*/
targetNode: ComfyWidgetNode;
/*
* Possible sources of values to insert into the target node
*/
sources: RestoreParamSource[]
}
export type RestoreParamTargets = Record<NodeID, RestoreParamTarget>
function isSerializedComfyWidgetNode(param: any): param is SerializedComfyWidgetNode {
return param != null && typeof param === "object" && "id" in param && "comfyValue" in param
}
function findUpstreamSerializedWidgetNode(prompt: SerializedPrompt, input: INodeInputSlot): [SerializedComfyWidgetNode | null, boolean | null] {
let linkID = input.link;
let isDirectAttachment = true;
while (linkID) {
const link = prompt.workflow.links[linkID]
if (link == null)
return [null, null];
const originNode = prompt.workflow.nodes.find(n => n.id === link[1])
if (isSerializedComfyWidgetNode(originNode))
return [originNode, isDirectAttachment]
isDirectAttachment = false;
// TODO: getUpstreamLink() for serialized nodes?
if (originNode.inputs && originNode.inputs.length === 1)
linkID = originNode.inputs[0].link
else
linkID = null;
}
return [null, null];
}
export default function restoreParameters(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets {
const result = {}
const addSource = (targetNode: ComfyWidgetNode, source: RestoreParamSource) => {
result[targetNode.id] ||= { targetNode, sources: [] }
result[targetNode.id].sources.push(source);
}
const graph = workflow.graph;
// Step 1: Find nodes that correspond to *this* workflow exactly, since we
// can easily match up the nodes between each (their IDs will be the same)
for (const serNode of prompt.workflow.nodes) {
const foundNode = graph.getNodeByIdRecursive(serNode.id);
if (isComfyWidgetNode(foundNode) && foundNode.type === serNode.type) {
const finalValue = (serNode as SerializedComfyWidgetNode).comfyValue;
if (finalValue != null) {
const source: RestoreParamSourceWorkflowNode = {
finalValue,
sourceNode: serNode
}
addSource(foundNode, source)
}
}
}
// Step 2: Figure out what parameters the backend received. If there was a
// widget node attached to a backend node's input upstream, then we can
// use that value.
for (const [serNodeID, inputs] of Object.entries(prompt.output)) {
const serNode = prompt.workflow.nodes.find(sn => sn.id === serNodeID)
if (serNode == null)
continue;
for (const [inputName, inputValue] of Object.entries(inputs)) {
const input = serNode.inputs.find(i => i.name === inputName);
if (input == null)
continue;
if (isSerializedPromptInputLink(inputValue))
continue;
const [originNode, isDirectAttachment] = findUpstreamSerializedWidgetNode(prompt, input)
if (originNode) {
const foundNode = graph.getNodeByIdRecursive(serNode.id);
if (isComfyWidgetNode(foundNode) && foundNode.type === serNode.type) {
const source: RestoreParamSourceBackendNodeInput = {
finalValue: inputValue,
backendNode: serNode,
isDirectAttachment
}
addSource(foundNode, source)
}
}
}
}
// Step 3: Extract the standard prompt from the workflow and use that to
// infer parameter types
const serializer = new ComfyBoxStdPromptSerializer();
const stdPrompt = serializer.serialize(prompt);
const allWidgetNodes = Array.from(graph.iterateNodesInOrderRecursive()).filter(isComfyWidgetNode);
for (const widgetNode of allWidgetNodes) {
}
// for (const [groupName, groups] of Object.entries(stdPrompt)) {
// }
return result;
}

View File

@@ -4,8 +4,8 @@ import type { FileData as GradioFileData } from "@gradio/upload";
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID, type SlotType, type Vector4, type SerializedLGraphNode } from "@litegraph-ts/core"; import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID, type SlotType, type Vector4, type SerializedLGraphNode } 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 ComfyApp, { type SerializedPrompt } from "./components/ComfyApp";
import workflowState, { type WorkflowReceiveOutputTargets } from "./stores/workflowState"; import workflowState, { type WorkflowReceiveOutputTargets } from "./stores/workflowState";
import ComfyApp, { type SerializedPrompt, type SerializedPromptInput, type SerializedPromptInputLink } from "./components/ComfyApp";
import { ImageViewer } from "./ImageViewer"; import { ImageViewer } from "./ImageViewer";
import configState from "$lib/stores/configState"; import configState from "$lib/stores/configState";
import SendOutputModal, { type SendOutputModalResult } from "$lib/components/modal/SendOutputModal.svelte"; import SendOutputModal, { type SendOutputModalResult } from "$lib/components/modal/SendOutputModal.svelte";
@@ -143,6 +143,10 @@ export function stopDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore)
layoutState.notifyWorkflowModified(); layoutState.notifyWorkflowModified();
}; };
export function isSerializedPromptInputLink(inputValue: SerializedPromptInput): inputValue is SerializedPromptInputLink {
return Array.isArray(inputValue) && inputValue.length === 2 && typeof inputValue[0] === "string" && typeof inputValue[1] === "number"
}
export function graphToGraphVis(graph: LGraph): string { export function graphToGraphVis(graph: LGraph): string {
let links: string[] = [] let links: string[] = []
let seenLinks = new Set() let seenLinks = new Set()
@@ -247,7 +251,7 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
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 (isSerializedPromptInputLink(i)) {
// Link // Link
const [inpID, inpSlot] = i; const [inpID, inpSlot] = i;
if (ids[inpID] == null) if (ids[inpID] == null)

View File

@@ -259,18 +259,18 @@
<Row> <Row>
{#if canMask} {#if canMask}
<div> <div>
{#if editMask} <Button variant="primary" disabled={!_value} on:click={toggleEditMask}>
<Button variant="secondary" on:click={() => { clearMask(); notify("Mask cleared."); }}>
Clear Mask
</Button>
{/if}
<Button disabled={!_value} on:click={toggleEditMask}>
{#if editMask} {#if editMask}
Show Image Show Image
{:else} {:else}
Edit Mask Edit Mask
{/if} {/if}
</Button> </Button>
{#if editMask}
<Button variant="secondary" on:click={() => { clearMask(); notify("Mask cleared."); }}>
Clear Mask
</Button>
{/if}
</div> </div>
{/if} {/if}
<div> <div>

View File

@@ -12,7 +12,7 @@ import { visualizer } from "rollup-plugin-visualizer";
const isProduction = process.env.NODE_ENV === "production"; const isProduction = process.env.NODE_ENV === "production";
console.log("Production build: " + isProduction) console.log("Production build: " + isProduction)
const commitHash = execSync('git rev-parse HEAD').toString(); const commitHash = execSync('git rev-parse HEAD').toString().trim();
console.log("Commit: " + commitHash) console.log("Commit: " + commitHash)
export default defineConfig({ export default defineConfig({