Start restore parameters
This commit is contained in:
@@ -224,13 +224,13 @@ const Metadata = z.object({
|
||||
extra_data: ExtraData
|
||||
})
|
||||
|
||||
const ComfyBoxStdPrompt = z.object({
|
||||
const StdPrompt = z.object({
|
||||
version: z.number(),
|
||||
metadata: Metadata,
|
||||
parameters: Parameters
|
||||
})
|
||||
|
||||
export default ComfyBoxStdPrompt
|
||||
export default StdPrompt
|
||||
|
||||
/*
|
||||
* 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
|
||||
* the frontend was the scale of 2.)
|
||||
*/
|
||||
export type ComfyBoxStdPrompt = z.infer<typeof ComfyBoxStdPrompt>
|
||||
export type ComfyBoxStdPrompt = z.infer<typeof StdPrompt>
|
||||
|
||||
@@ -1,31 +1,88 @@
|
||||
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;
|
||||
|
||||
function LoraLoader(stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs) {
|
||||
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]
|
||||
export type ComfyPromptConverter = {
|
||||
encoder: ComfyPromptEncoder,
|
||||
decoder: ComfyPromptDecoder
|
||||
}
|
||||
|
||||
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__;
|
||||
|
||||
export default class ComfyBoxStdPromptSerializer {
|
||||
serialize(prompt: SerializedPrompt): ComfyBoxStdPrompt {
|
||||
serialize(prompt: SerializedPromptInputsAll, workflow?: SerializedAppState): [SafeParseReturnType<any, ComfyBoxStdPrompt>, any] {
|
||||
const stdPrompt: ComfyBoxStdPrompt = {
|
||||
version: 1,
|
||||
metadata: {
|
||||
@@ -33,23 +90,57 @@ export default class ComfyBoxStdPromptSerializer {
|
||||
commit_hash: COMMIT_HASH,
|
||||
extra_data: {
|
||||
comfybox: {
|
||||
workflows: [] // TODO!!!
|
||||
}
|
||||
}
|
||||
},
|
||||
parameters: {}
|
||||
}
|
||||
|
||||
for (const [nodeID, inputs] of Object.entries(prompt.output)) {
|
||||
for (const [nodeID, inputs] of Object.entries(prompt)) {
|
||||
const classType = inputs.class_type
|
||||
const converter = ALL_CONVERTERS[classType]
|
||||
if (converter) {
|
||||
converter(stdPrompt, inputs.inputs, nodeID)
|
||||
const spec = ALL_SPECS[classType]
|
||||
if (spec) {
|
||||
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 {
|
||||
target = target[name]
|
||||
}
|
||||
console.warn(index, name, target)
|
||||
}
|
||||
|
||||
let name = trail[trail.length - 1]
|
||||
target[name] = inputValue
|
||||
console.warn(stdPrompt.parameters)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 converter for comfy class!", classType)
|
||||
console.warn("No StdPrompt type spec for comfy class!", classType)
|
||||
}
|
||||
}
|
||||
|
||||
return stdPrompt
|
||||
return [StdPrompt.safeParse(stdPrompt), stdPrompt];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1064,9 +1064,6 @@ export default class ComfyApp {
|
||||
// console.debug(graphToGraphVis(workflow.graph))
|
||||
// console.debug(promptToGraphVis(p))
|
||||
|
||||
const stdPrompt = this.stdPromptSerializer.serialize(p);
|
||||
// console.warn("STD", stdPrompt);
|
||||
|
||||
const extraData: ComfyBoxPromptExtraData = {
|
||||
extra_pnginfo: {
|
||||
comfyBoxWorkflow: wf,
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
|
||||
import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte";
|
||||
import uiQueueState from "$lib/stores/uiQueueState";
|
||||
import type { SerializedAppState, SerializedPromptInputsAll } from "./ComfyApp";
|
||||
|
||||
export let app: ComfyApp;
|
||||
|
||||
@@ -124,19 +125,22 @@
|
||||
|
||||
let showModal = false;
|
||||
let expandAll = false;
|
||||
let selectedPrompt = null;
|
||||
let selectedPrompt: SerializedPromptInputsAll | null = null;
|
||||
let selectedWorkflow: SerializedAppState | null = null;
|
||||
let selectedImages = [];
|
||||
function showPrompt(entry: QueueUIEntry) {
|
||||
if (entry.error != null) {
|
||||
showModal = false;
|
||||
expandAll = false;
|
||||
selectedPrompt = null;
|
||||
selectedWorkflow = null;
|
||||
selectedImages = [];
|
||||
|
||||
showError(entry.entry.promptID);
|
||||
}
|
||||
else {
|
||||
selectedPrompt = entry.entry.prompt;
|
||||
selectedPrompt = entry.entry.prompt,
|
||||
selectedWorkflow = entry.entry.extraData.extra_pnginfo.comfyBoxWorkflow
|
||||
selectedImages = entry.images;
|
||||
showModal = true;
|
||||
expandAll = false
|
||||
@@ -145,6 +149,7 @@
|
||||
|
||||
function closeModal() {
|
||||
selectedPrompt = null
|
||||
selectedWorkflow = null;
|
||||
selectedImages = []
|
||||
showModal = false;
|
||||
expandAll = false;
|
||||
@@ -165,7 +170,7 @@
|
||||
</div>
|
||||
<svelte:fragment let:closeDialog>
|
||||
{#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}
|
||||
</svelte:fragment>
|
||||
<div slot="buttons" let:closeDialog>
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
}
|
||||
|
||||
.comfy-settings-entries {
|
||||
padding: 3rem 3rem;
|
||||
padding: 2rem 0.75rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 { JSON as JSONComponent } from "@gradio/json";
|
||||
import { JSON as JSONIcon, Copy, Check } from "@gradio/icons";
|
||||
@@ -13,16 +13,39 @@
|
||||
import workflowState, { type ComfyBoxWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
|
||||
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
|
||||
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;
|
||||
|
||||
export let prompt: SerializedPromptInputsAll;
|
||||
export let workflow: SerializedAppState | null;
|
||||
export let images: string[] = []; // list of image URLs to ComfyUI's /view? endpoint
|
||||
export let isMobile: boolean = false;
|
||||
export let expandAll: boolean = false;
|
||||
export let closeModal: () => void;
|
||||
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 galleryStyle: Styles = {
|
||||
@@ -127,62 +150,14 @@
|
||||
|
||||
<div class="prompt-display">
|
||||
<div class="prompt-and-sends">
|
||||
<Block>
|
||||
<Accordion label="Prompt" open={expandAll || comfyBoxImages.length === 0}>
|
||||
<div class="scroll-container">
|
||||
<Block>
|
||||
{#each Object.entries(prompt) as [nodeID, inputs], i}
|
||||
{@const classType = inputs.class_type}
|
||||
{@const filtered = Object.entries(inputs.inputs).filter((i) => !isInputLink(i[1]))}
|
||||
{#if filtered.length > 0}
|
||||
<div class="accordion">
|
||||
<Block padding={true}>
|
||||
<Accordion label="Node {i+1}: {classType}" open={expandAll}>
|
||||
{#each filtered as [inputName, input]}
|
||||
<Block>
|
||||
<button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}>
|
||||
{#if copiedNodeID === nodeID && copiedInputName === inputName}
|
||||
<span class="copied-icon">
|
||||
<Check />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="copy-text"><Copy /></span>
|
||||
{/if}
|
||||
</button>
|
||||
<div>
|
||||
{#if isInputLink(input)}
|
||||
Link {input[0]} -> {input[1]}
|
||||
{:else if typeof input === "object"}
|
||||
<Block>
|
||||
<BlockLabel
|
||||
Icon={JSONIcon}
|
||||
show_label={true}
|
||||
label={inputName}
|
||||
float={true}
|
||||
/>
|
||||
<JSONComponent value={input} />
|
||||
</Block>
|
||||
{:else if isMultiline(input)}
|
||||
{@const lines = Math.max(countNewLines(input), input.length / splitLength)}
|
||||
<TextBox label={inputName} value={formatInput(input)} {lines} max_lines={lines} />
|
||||
{:else}
|
||||
<TextBox label={inputName} value={formatInput(input)} lines={1} max_lines={1} />
|
||||
{/if}
|
||||
</div>
|
||||
</Block>
|
||||
{/each}
|
||||
</Accordion>
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</Block>
|
||||
</div>
|
||||
</Accordion>
|
||||
</Block>
|
||||
{#if comfyBoxImages.length > 0}
|
||||
<Block>
|
||||
<Accordion label="Send Outputs To..." open={true}>
|
||||
<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}
|
||||
@@ -191,9 +166,94 @@
|
||||
<div class="outputs-message">No receive output targets found across all workflows.</div>
|
||||
{/if}
|
||||
</Block>
|
||||
</Accordion>
|
||||
</Block>
|
||||
{/if}
|
||||
</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>
|
||||
<div class="scroll-container">
|
||||
<Block>
|
||||
{#each Object.entries(prompt) as [nodeID, inputs], i}
|
||||
{@const classType = inputs.class_type}
|
||||
{@const filtered = Object.entries(inputs.inputs).filter((i) => !isInputLink(i[1]))}
|
||||
{#if filtered.length > 0}
|
||||
<div class="accordion">
|
||||
<Block padding={true}>
|
||||
<Accordion label="Node {i+1}: {classType}" open={expandAll}>
|
||||
{#each filtered as [inputName, input]}
|
||||
<Block>
|
||||
<button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}>
|
||||
{#if copiedNodeID === nodeID && copiedInputName === inputName}
|
||||
<span class="copied-icon">
|
||||
<Check />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="copy-text"><Copy /></span>
|
||||
{/if}
|
||||
</button>
|
||||
<div>
|
||||
{#if isInputLink(input)}
|
||||
Link {input[0]} -> {input[1]}
|
||||
{:else if typeof input === "object"}
|
||||
<Block>
|
||||
<BlockLabel
|
||||
Icon={JSONIcon}
|
||||
show_label={true}
|
||||
label={inputName}
|
||||
float={true}
|
||||
/>
|
||||
<JSONComponent value={input} />
|
||||
</Block>
|
||||
{:else if isMultiline(input)}
|
||||
{@const lines = Math.max(countNewLines(input), input.length / splitLength)}
|
||||
<TextBox label={inputName} value={formatInput(input)} {lines} max_lines={lines} />
|
||||
{:else}
|
||||
<TextBox label={inputName} value={formatInput(input)} lines={1} max_lines={1} />
|
||||
{/if}
|
||||
</div>
|
||||
</Block>
|
||||
{/each}
|
||||
</Accordion>
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</Block>
|
||||
</div>
|
||||
</Block>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</div>
|
||||
{#if images.length > 0}
|
||||
<div class="image-container">
|
||||
@@ -221,21 +281,34 @@
|
||||
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-y: auto;
|
||||
|
||||
flex-direction: column;
|
||||
@media (min-width: 1600px) {
|
||||
@media (min-width: 1200px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
/* overflow-y: auto; */
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.json {
|
||||
@include json-view;
|
||||
}
|
||||
|
||||
.prompt-and-sends {
|
||||
width: 50%;
|
||||
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
/* overflow-y: auto; */
|
||||
flex: 1 1 0%;
|
||||
overflow-y: auto;
|
||||
|
||||
:global(>.tabs) {
|
||||
height: 100%;
|
||||
|
||||
:global(>.tabitem) {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
|
||||
205
src/lib/restoreParameters.ts
Normal file
205
src/lib/restoreParameters.ts
Normal 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;
|
||||
}
|
||||
@@ -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 { get } from "svelte/store";
|
||||
import type { ComfyNodeID } from "./api";
|
||||
import ComfyApp, { type SerializedPrompt } from "./components/ComfyApp";
|
||||
import workflowState, { type WorkflowReceiveOutputTargets } from "./stores/workflowState";
|
||||
import ComfyApp, { type SerializedPrompt, type SerializedPromptInput, type SerializedPromptInputLink } from "./components/ComfyApp";
|
||||
import { ImageViewer } from "./ImageViewer";
|
||||
import configState from "$lib/stores/configState";
|
||||
import SendOutputModal, { type SendOutputModalResult } from "$lib/components/modal/SendOutputModal.svelte";
|
||||
@@ -143,6 +143,10 @@ export function stopDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore)
|
||||
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 {
|
||||
let links: string[] = []
|
||||
let seenLinks = new Set()
|
||||
@@ -247,7 +251,7 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
|
||||
for (const pair2 of Object.entries(o.inputs)) {
|
||||
const [inpName, i] = pair2;
|
||||
|
||||
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
|
||||
if (isSerializedPromptInputLink(i)) {
|
||||
// Link
|
||||
const [inpID, inpSlot] = i;
|
||||
if (ids[inpID] == null)
|
||||
|
||||
@@ -259,18 +259,18 @@
|
||||
<Row>
|
||||
{#if canMask}
|
||||
<div>
|
||||
{#if editMask}
|
||||
<Button variant="secondary" on:click={() => { clearMask(); notify("Mask cleared."); }}>
|
||||
Clear Mask
|
||||
</Button>
|
||||
{/if}
|
||||
<Button disabled={!_value} on:click={toggleEditMask}>
|
||||
<Button variant="primary" disabled={!_value} on:click={toggleEditMask}>
|
||||
{#if editMask}
|
||||
Show Image
|
||||
{:else}
|
||||
Edit Mask
|
||||
{/if}
|
||||
</Button>
|
||||
{#if editMask}
|
||||
<Button variant="secondary" on:click={() => { clearMask(); notify("Mask cleared."); }}>
|
||||
Clear Mask
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { visualizer } from "rollup-plugin-visualizer";
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
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)
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
Reference in New Issue
Block a user