Start restore parameters
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
decoder: ComfyPromptDecoder
|
||||||
|
}
|
||||||
|
|
||||||
function LoraLoader(stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs) {
|
//
|
||||||
|
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 params = stdPrompt.parameters
|
||||||
|
const loras: ComfyBoxStdGroupLoRA[] = params.lora
|
||||||
|
|
||||||
const lora: ComfyBoxStdGroupLoRA = {
|
for (const lora of loras) {
|
||||||
model_name: inputs["lora_name"],
|
lora.model_hashes = {
|
||||||
strength_unet: inputs["strength_model"],
|
addnet_shorthash: null // TODO find hashes for model!
|
||||||
strength_tenc: inputs["strength_clip"]
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.lora)
|
// input name -> group/key in standard prompt
|
||||||
params.lora.push(lora)
|
type ComfyStdPromptMapping = Record<string, string>
|
||||||
else
|
|
||||||
params.lora = [lora]
|
type ComfyStdPromptSpec = {
|
||||||
|
paramMapping: ComfyStdPromptMapping,
|
||||||
|
extraParams?: Record<string, string>,
|
||||||
|
converter?: ComfyPromptConverter,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALL_CONVERTERS: Record<string, ComfyPromptConverter> = {
|
const ALL_SPECS: Record<string, ComfyStdPromptSpec> = {
|
||||||
LoraLoader
|
"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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -173,7 +173,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comfy-settings-entries {
|
.comfy-settings-entries {
|
||||||
padding: 3rem 3rem;
|
padding: 2rem 0.75rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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 { 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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user