From c7ad04b69adceee8fa56917cfb4711f05b746f77 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Thu, 18 May 2023 15:14:18 -0500 Subject: [PATCH 01/14] Commentz --- src/lib/components/ComfyApp.ts | 42 +++++++++++++++++++-- src/lib/components/ComfyPromptSerializer.ts | 2 +- src/lib/nodes/ComfyActionNodes.ts | 4 +- src/lib/stores/layoutState.ts | 26 ++++++++----- src/lib/stores/queueState.ts | 34 +++++++++++++---- 5 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 295f112..bc2331a 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -45,25 +45,49 @@ if (typeof window !== "undefined") { nodes.ComfyReroute.setDefaultTextVisibility(!!localStorage["Comfy.ComfyReroute.DefaultVisibility"]); } -type QueueItem = { num: number, batchCount: number } +/* + * Queued prompt that hasn't been sent to the backend yet. + * TODO: Assumes the currently active graph will be serialized, needs to change + * for multiple loaded workflow support + */ +type QueueItem = { + num: number, + batchCount: number +} +/* + * Represents a single workflow that can be loaded into the program from JSON. + */ export type SerializedAppState = { + /** Program identifier, should always be "ComfyBox" */ createdBy: "ComfyBox", + /** Serial version, should be incremented on breaking changes */ version: number, + /** Commit hash if found */ + commitHash?: string, + /** Graph state */ workflow: SerializedLGraph, + /** UI state */ layout: SerializedLayoutState, + /** Position/offset of the canvas at the time of saving */ canvas: SerializedGraphCanvasState } -/** [link origin, link index] | value */ +/** [link_origin, link_slot_index] | input_value */ export type SerializedPromptInput = [ComfyNodeID, number] | any +/* + * A single node in the prompt and its input values. + */ export type SerializedPromptInputs = { /* property name -> value or link */ inputs: Record, class_type: string } +/* + * All nodes in the graph and their input values. + */ export type SerializedPromptInputsAll = Record export type SerializedPrompt = { @@ -71,6 +95,9 @@ export type SerializedPrompt = { output: SerializedPromptInputsAll } +/* + * Outputs for each node. + */ export type SerializedPromptOutputs = Record export type Progress = { @@ -78,6 +105,10 @@ export type Progress = { max: number } +/* + * A combo node and the backend node that will send an updated config over, for + * refreshing lists of model files + */ type BackendComboNode = { comboNode: ComfyComboNode, comfyInput: IComfyInputSlot, @@ -95,6 +126,7 @@ export default class ComfyApp { nodeOutputs: Record = {}; shiftDown: boolean = false; + ctrlDown: boolean = false; selectedGroupMoving: boolean = false; private queueItems: QueueItem[] = []; @@ -406,6 +438,7 @@ export default class ComfyApp { private addKeyboardHandler() { window.addEventListener("keydown", (e) => { this.shiftDown = e.shiftKey; + this.ctrlDown = e.ctrlKey; // Queue prompt using ctrl or command + enter if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.keyCode === 13 || e.keyCode === 10)) { @@ -414,6 +447,7 @@ export default class ComfyApp { }); window.addEventListener("keyup", (e) => { this.shiftDown = e.shiftKey; + this.ctrlDown = e.ctrlKey; }); } @@ -561,7 +595,9 @@ export default class ComfyApp { } if (get(layoutState).attrs.queuePromptButtonRunWorkflow) { - this.queuePrompt(0, 1); + // Hold control to queue at the front + const num = this.ctrlDown ? -1 : 0; + this.queuePrompt(num, 1); } } diff --git a/src/lib/components/ComfyPromptSerializer.ts b/src/lib/components/ComfyPromptSerializer.ts index 878504d..253d40d 100644 --- a/src/lib/components/ComfyPromptSerializer.ts +++ b/src/lib/components/ComfyPromptSerializer.ts @@ -113,7 +113,7 @@ export class UpstreamNodeLocator { } // If there are non-target nodes between us and another - // backend node, we have to traverse them first. This + // target node, we have to traverse them first. This // behavior is dependent on the type of node. Reroute nodes // will simply follow their single input, while branching // nodes have conditional logic that determines which link diff --git a/src/lib/nodes/ComfyActionNodes.ts b/src/lib/nodes/ComfyActionNodes.ts index 2da838a..9e083a3 100644 --- a/src/lib/nodes/ComfyActionNodes.ts +++ b/src/lib/nodes/ComfyActionNodes.ts @@ -312,7 +312,9 @@ export class ComfyExecuteSubgraphAction extends ComfyGraphNode { if (!app) return; - app.queuePrompt(0, 1, tag); + // Hold control to queue at the front + const num = app.ctrlDown ? -1 : 0; + app.queuePrompt(num, 1, tag); } } diff --git a/src/lib/stores/layoutState.ts b/src/lib/stores/layoutState.ts index e3568c6..82b7c42 100644 --- a/src/lib/stores/layoutState.ts +++ b/src/lib/stores/layoutState.ts @@ -39,8 +39,10 @@ export type LayoutAttributes = { queuePromptButtonName: string, /* - * If true, clicking the "Queue Prompt" button will run the default subgraph. - * Set this to false if you need special behavior before running any subgraphs. + * If true, clicking the "Queue Prompt" button will run the default + * subgraph. Set this to false if you need special behavior before running + * any subgraphs, and instead use the `onDefaultQueueAction` event of the + * Comfy.QueueEvents node. */ queuePromptButtonRunWorkflow: boolean, } @@ -84,6 +86,9 @@ export type LayoutState = { */ attrs: LayoutAttributes + /* + * Increment to force Svelte to re-render the props panel + */ refreshPropsPanel: Writable } @@ -102,7 +107,7 @@ export type Attributes = { title: string, /* - * List of classes to apply to the component. + * List of CSS classes to apply to the component. */ classes: string, @@ -204,22 +209,22 @@ export type AttributesSpec = { values?: string[], /* - * If `type` is "number", step for the slider + * If `type` is "number", step for the slider that edits this attribute */ step?: number, /* - * If `type` is "number", min for the slider + * If `type` is "number", min for the slider that edits this attribute */ min?: number, /* - * If `type` is "number", max for the slider + * If `type` is "number", max for the slider that edits this attribute */ max?: number, /* - * If `type` is "string", display as a textarea. + * If `type` is "string", display as a textarea instead of an input. */ multiline?: boolean, @@ -863,8 +868,9 @@ function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) { let prevWidget = state.allItemsByNode[node.id] if (prevWidget == null) { // If a subgraph was cloned, try looking for the original widget node corresponding to the new widget node being added. - // `node` is the new ComfyWidgetNode instance to copy attrs to. - // `options.cloneData` should contain the results of Subgraph.clone(), called "subgraphNewIDMapping". + // `node` is the new ComfyWidgetNode instance to copy layout attrs to. + // `options.cloneData` should contain the results of Subgraph.clone(), which is named "subgraphNewIDMapping" in an + // entry of the `forNode` Record. // `options.cloneData` is attached to the onNodeAdded options if a node is added to a graph after being // selection-cloned or pasted, as they both call clone() internally. const cloneData = options.cloneData.forNode[options.prevNodeID] @@ -879,7 +885,7 @@ function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) { if (nodeIDInLayoutState) { // Gottem. prevWidget = state.allItemsByNode[nodeIDInLayoutState] - console.warn("FOUND CLONED SUBGRAPH NODE", node.id, "=>", nodeIDInLayoutState, prevWidget) + // console.warn("FOUND CLONED SUBGRAPH NODE", node.id, "=>", nodeIDInLayoutState, prevWidget) } } } diff --git a/src/lib/stores/queueState.ts b/src/lib/stores/queueState.ts index bcc2b6c..ebe8a3d 100644 --- a/src/lib/stores/queueState.ts +++ b/src/lib/stores/queueState.ts @@ -4,10 +4,6 @@ import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes"; import notify from "$lib/notify"; import { get, writable, type Writable } from "svelte/store"; -export type QueueItem = { - name: string -} - export type QueueEntryStatus = "success" | "error" | "interrupted" | "all_cached" | "unknown"; type QueueStateOps = { @@ -23,8 +19,13 @@ type QueueStateOps = { onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => void } +/* + * Single job that the backend keeps track of. + */ export type QueueEntry = { - /* Data preserved on page refresh */ + /*** Data preserved on page refresh ***/ + + /** Priority of the prompt. -1 means to queue at the front. */ number: number, queuedAt?: Date, finishedAt?: Date, @@ -33,23 +34,34 @@ export type QueueEntry = { extraData: ComfyBoxPromptExtraData, goodOutputs: ComfyNodeID[], - /* Data not sent by ComfyUI's API, lost on page refresh */ + /*** Data not sent by ComfyUI's API, lost on page refresh ***/ /* Prompt outputs, collected while the prompt is still executing */ outputs: SerializedPromptOutputs, - - /* Nodes in of the workflow that have finished running so far. */ + /* Nodes of the workflow that have finished running so far. */ nodesRan: Set, + /* Nodes of the workflow the backend reported as cached. */ cachedNodes: Set } +/* + * Represents a queue entry that has finished executing (suceeded or failed) and + * has been moved to the history. + */ export type CompletedQueueEntry = { + /** Corresponding entry in the queue, for the prompt/extra data */ entry: QueueEntry, + /** The result of this prompt, success/failed/cached */ status: QueueEntryStatus, + /** Message to display in the frontend */ message?: string, + /** Detailed error/stacktrace, perhaps inspectible with a popup */ error?: string, } +/* + * Keeps track of queued and completed (history) prompts. + */ export type QueueState = { queueRunning: Writable, queuePending: Writable, @@ -57,6 +69,11 @@ export type QueueState = { queueRemaining: number | "X" | null; runningNodeID: ComfyNodeID | null; progress: Progress | null, + /** + * If true, user pressed the "Interrupt" button in the frontend. Disable the + * button and wait until the next prompt starts running to re-enable it + * again + */ isInterrupting: boolean } type WritableQueueStateStore = Writable & QueueStateOps; @@ -159,6 +176,7 @@ function moveToCompleted(index: number, queue: Writable, status: Q return qc }) + state.isInterrupting = false; store.set(state) } From 54bcc04d8879263a60c3317b591e40a8f1054548 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Thu, 18 May 2023 19:50:23 -0500 Subject: [PATCH 02/14] Converter for A1111 infotexts to standardized format --- package.json | 3 +- pnpm-lock.yaml | 47 ++++- src/lib/ComfyBoxStdPrompt.ts | 155 ++++++++++++++ src/lib/convertA1111ToStdPrompt.ts | 213 ++++++++++++++++++++ src/lib/parseA1111.ts | 166 +++++++++++++++ src/tests/convertA1111ToStdPromptTests.ts | 234 ++++++++++++++++++++++ src/tests/parseA1111Tests.ts | 158 +++++++++++++++ src/tests/testSuite.ts | 2 + tsconfig.json | 1 + 9 files changed, 967 insertions(+), 12 deletions(-) create mode 100644 src/lib/ComfyBoxStdPrompt.ts create mode 100644 src/lib/convertA1111ToStdPrompt.ts create mode 100644 src/lib/parseA1111.ts create mode 100644 src/tests/convertA1111ToStdPromptTests.ts create mode 100644 src/tests/parseA1111Tests.ts diff --git a/package.json b/package.json index 46c87bb..9eafe53 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "tailwindcss": "^3.3.1", "typed-emitter": "github:andywer/typed-emitter", "uuid": "^9.0.0", - "vite-plugin-full-reload": "^1.0.5" + "vite-plugin-full-reload": "^1.0.5", + "zod": "^3.21.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06df85c..cc388df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: vite-plugin-full-reload: specifier: ^1.0.5 version: 1.0.5(vite@4.3.1) + zod: + specifier: ^3.21.4 + version: 3.21.4 devDependencies: '@floating-ui/core': specifier: ^1.2.6 @@ -3126,10 +3129,14 @@ packages: /@types/chai-subset@1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: - '@types/chai': 4.3.4 + '@types/chai': 4.3.5 /@types/chai@4.3.4: resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} + dev: false + + /@types/chai@4.3.5: + resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} /@types/concat-stream@1.6.1: resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==} @@ -3691,8 +3698,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001481 - electron-to-chromium: 1.4.371 + caniuse-lite: 1.0.30001488 + electron-to-chromium: 1.4.400 node-releases: 2.0.10 update-browserslist-db: 1.0.11(browserslist@4.21.5) @@ -3753,6 +3760,10 @@ packages: /caniuse-lite@1.0.30001481: resolution: {integrity: sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==} + dev: true + + /caniuse-lite@1.0.30001488: + resolution: {integrity: sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==} /case@1.6.3: resolution: {integrity: sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==} @@ -4449,8 +4460,8 @@ packages: sigmund: 1.0.1 dev: false - /electron-to-chromium@1.4.371: - resolution: {integrity: sha512-jlBzY4tFcJaiUjzhRTCWAqRvTO/fWzjA3Bls0mykzGZ7zvcMP7h05W6UcgzfT9Ca1SW2xyKDOFRyI0pQeRNZGw==} + /electron-to-chromium@1.4.400: + resolution: {integrity: sha512-Lsvf7cvwbIxCfB8VqbnVtEsjGi3+48ejDiQZfWo5gkT+1vQ2DHQI5pl0nUvPD6z1IQk6JgFeMC5ZQJqVhalEHg==} /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -7513,11 +7524,11 @@ packages: resolution: {integrity: sha512-ASEq9atUw7lualXB+knvgtvwkCEvGWV2gDD/8qnASzBkzEARZck9JAyxmY8OS6Nc1pCPEgDTKNcx+YqqYfzArw==} dev: false - /rxjs@7.8.0: - resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} requiresBuild: true dependencies: - tslib: 2.5.0 + tslib: 2.5.1 dev: false optional: true @@ -8314,6 +8325,11 @@ packages: /tinybench@2.4.0: resolution: {integrity: sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==} + dev: false + + /tinybench@2.5.0: + resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} + dev: true /tinypool@0.3.1: resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==} @@ -8420,6 +8436,11 @@ packages: /tslib@2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + /tslib@2.5.1: + resolution: {integrity: sha512-KaI6gPil5m9vF7DKaoXxx1ia9fxS4qG5YveErRRVknPDXXriu5M8h48YRjB6h5ZUOKuAKlSJYb0GaDe8I39fRw==} + dev: false + optional: true + /tsutils@3.21.0(typescript@5.0.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -9401,7 +9422,7 @@ packages: jsdom: optional: true dependencies: - '@types/chai': 4.3.4 + '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 '@types/node': 18.16.0 acorn: 8.8.2 @@ -9416,7 +9437,7 @@ packages: source-map: 0.6.1 std-env: 3.3.3 strip-literal: 1.0.1 - tinybench: 2.4.0 + tinybench: 2.5.0 tinypool: 0.3.1 tinyspy: 1.1.1 vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0) @@ -9752,10 +9773,14 @@ packages: commander: 9.5.0 dev: false + /zod@3.21.4: + resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + dev: false + github.com/andywer/typed-emitter/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4: resolution: {tarball: https://codeload.github.com/andywer/typed-emitter/tar.gz/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4} name: typed-emitter version: 2.1.0 optionalDependencies: - rxjs: 7.8.0 + rxjs: 7.8.1 dev: false diff --git a/src/lib/ComfyBoxStdPrompt.ts b/src/lib/ComfyBoxStdPrompt.ts new file mode 100644 index 0000000..de0c66a --- /dev/null +++ b/src/lib/ComfyBoxStdPrompt.ts @@ -0,0 +1,155 @@ +import { z, type ZodTypeAny } from "zod" + +const ModelHashes = z.object({ + a1111_shorthash: z.string().optional(), + sha256: z.string().optional(), +}).refine(({ a1111_shorthash, sha256 }) => + a1111_shorthash !== undefined || sha256 !== undefined, + { message: "At least one model hash must be specified" }) + +const GroupPrompt = z.object({ + positive: z.string(), + negative: z.string() +}) +export type ComfyBoxStdGroupPrompt = z.infer + +const GroupCheckpoint = z.object({ + model_name: z.string().optional(), + model_hashes: ModelHashes.optional(), +}).refine(({ model_name, model_hashes }) => + model_name !== undefined || model_hashes !== undefined, + { message: "Must include either model name or model hash" } +) +export type ComfyBoxStdGroupCheckpoint = z.infer + +const GroupVAE = z.object({ + model_name: z.string().optional(), + model_hashes: ModelHashes.optional(), + type: z.enum(["internal", "external"]) +}).refine(({ model_name, model_hashes }) => + model_name !== undefined || model_hashes !== undefined, + { message: "Must include either model name or model hashes" } +) +export type ComfyBoxStdGroupVAE = z.infer + +const GroupKSampler = z.object({ + cfg_scale: z.number(), + seed: z.number(), + steps: z.number(), + sampler_name: z.string(), + scheduler: z.string(), + denoise: z.number().default(1.0) +}) +export type ComfyBoxStdGroupKSampler = z.infer + +const GroupLatentImage = z.object({ + width: z.number(), + height: z.number(), + type: z.enum(["empty", "image", "image_upscale"]).optional(), + upscale_method: z.string().optional(), + upscale_by: z.number().optional(), + upscale_width: z.number().optional(), + upscale_height: z.number().optional(), + crop: z.string().optional(), + mask_blur: z.number().optional(), + batch_count: z.number().default(1).optional(), + batch_pos: z.number().default(0).optional() +}) +export type ComfyBoxStdGroupLatentImage = z.infer + +const GroupSDUpscale = z.object({ + upscaler: z.string(), + overlap: z.number(), +}) +export type ComfyBoxStdGroupSDUpscale = z.infer + +const GroupHypernetwork = z.object({ + model_name: z.string(), + model_hashes: ModelHashes.optional(), + strength: z.number() +}) +export type ComfyBoxStdGroupHypernetwork = z.infer + +const LoRAModelHashes = z.object({ + addnet_shorthash: z.string().optional(), + addnet_shorthash_legacy: z.string().optional(), + sha256: z.string().optional(), +}).refine(({ addnet_shorthash, addnet_shorthash_legacy, sha256 }) => + addnet_shorthash !== undefined || addnet_shorthash_legacy !== undefined || sha256 !== undefined, + { message: "At least one model hash must be specified" }) + +const GroupLoRA = z.object({ + model_name: z.string(), + module_name: z.string().optional(), + model_hashes: LoRAModelHashes.optional(), + strength_unet: z.number(), + strength_tenc: z.number() +}) +export type ComfyBoxStdGroupLoRA = z.infer + +const GroupControlNet = z.object({ + model: z.string(), + model_hashes: ModelHashes.optional(), + strength: z.number(), +}) +export type ComfyBoxStdGroupControlNet = z.infer + +const GroupCLIP = z.object({ + clip_skip: z.number().optional() +}) +export type ComfyBoxStdGroupCLIP = z.infer + +const GroupDynamicThresholding = z.object({ + mimic_scale: z.number(), + threshold_percentile: z.number(), + mimic_mode: z.string(), + mimic_scale_min: z.number(), + cfg_mode: z.string(), + cfg_scale_minimum: z.number() +}) +export type ComfyBoxStdGroupDynamicThresholding = z.infer + +const group = (s: ZodTypeAny) => z.optional(z.array(s).nonempty()); + +const Parameters = z.object({ + prompt: group(GroupPrompt), + checkpoint: group(GroupCheckpoint), + vae: group(GroupVAE), + k_sampler: group(GroupKSampler), + clip: group(GroupCLIP), + latent_image: group(GroupLatentImage), + sd_upscale: group(GroupSDUpscale), + hypernetwork: group(GroupHypernetwork), + lora: group(GroupLoRA), + control_net: group(GroupControlNet), + dynamic_thresholding: group(GroupDynamicThresholding) +}).partial() +export type ComfyBoxStdParameters = z.infer + +const ComfyBoxExtraData = z.object({ + workflows: z.array(z.string()) +}) + +const ExtraData = z.object({ + comfybox: ComfyBoxExtraData.optional() +}) + +const Metadata = z.object({ + version: z.number(), + created_with: z.string(), + author: z.string().optional(), + commit_hash: z.string().optional(), + extra_data: ExtraData +}) + +const Prompt = z.object({ + metadata: Metadata, + parameters: Parameters +}) + +const ComfyBoxStdPrompt = z.object({ + prompt: Prompt, +}) + +export default ComfyBoxStdPrompt +export type ComfyBoxStdPrompt = z.infer diff --git a/src/lib/convertA1111ToStdPrompt.ts b/src/lib/convertA1111ToStdPrompt.ts new file mode 100644 index 0000000..8726a5e --- /dev/null +++ b/src/lib/convertA1111ToStdPrompt.ts @@ -0,0 +1,213 @@ +import type { ComfyBoxStdGroupCheckpoint, ComfyBoxStdGroupHypernetwork, ComfyBoxStdGroupKSampler, ComfyBoxStdGroupLatentImage, ComfyBoxStdGroupLoRA, ComfyBoxStdParameters, ComfyBoxStdPrompt } from "./ComfyBoxStdPrompt"; +import type { A1111ParsedInfotext } from "./parseA1111"; + +function getSamplerAndScheduler(a1111Sampler: string): [string, string] { + let name = a1111Sampler.toLowerCase().replace("++", "pp").replaceAll(" ", "_"); + let scheduler = "normal"; + if (name.includes("karras")) { + name = name.replace("karras", "").replace(/_+$/, ""); + scheduler = "karras"; + } else { + scheduler = "normal" + } + return [name, scheduler] +} + +const reAddNetModelName = /^([^(]+)\(([^)]+)\)$/; +const reParens = /\(([^)]+)\)/; + +function parseAddNetModelNameAndHash(name: string): [string | null, string | null] { + const match = name.match(reAddNetModelName); + if (match) { + return [match[1], match[2]] + } + return [null, null] +} + +export default function convertA1111ToStdPrompt(infotext: A1111ParsedInfotext): ComfyBoxStdPrompt { + const popOpt = (name: string): string | undefined => { + const v = infotext.extraParams[name]; + delete infotext.extraParams[name]; + return v; + } + + const parameters: ComfyBoxStdParameters = {} + + const hrUp = popOpt("hires upscale"); + const hrSz = popOpt("hires resize"); + let hrMethod = popOpt("hires upscaler"); + let hrWidth = undefined + let hrHeight = undefined + if (hrSz) { + [hrWidth, hrHeight] = hrSz.split(hrSz).map(parseInt); + } + + if (hrMethod != null && hrMethod.startsWith("Latent (")) { + const result = reParens.exec(hrMethod) + if (result) + hrMethod = String(result[1]) + } + + const latent_image: ComfyBoxStdGroupLatentImage = { + width: infotext.width, + height: infotext.height, + upscale_method: hrMethod, + upscale_by: hrUp ? parseFloat(hrUp) : undefined, + upscale_width: hrWidth, + upscale_height: hrHeight, + batch_count: infotext.batchSize, + batch_pos: infotext.batchPos, + } + + const maskBlur = popOpt("mask blur") + if (maskBlur != null) + latent_image.mask_blur = parseFloat(maskBlur) + + parameters.latent_image = [latent_image]; + + const [sampler_name, scheduler] = getSamplerAndScheduler(infotext.sampler) + + const k_sampler: ComfyBoxStdGroupKSampler = { + steps: infotext.steps, + seed: infotext.seed, + cfg_scale: infotext.cfgScale, + denoise: infotext.denoise || 1.0, + sampler_name, + scheduler, + } + parameters.k_sampler = [k_sampler]; + + if (infotext.modelHash || infotext.modelName) { + const checkpoint: ComfyBoxStdGroupCheckpoint = { + model_name: infotext.modelName, + model_hashes: { + a1111_shorthash: infotext.modelHash + } + } + parameters.checkpoint = [checkpoint] + } + + const clipSkip = popOpt("clip skip") + if (clipSkip != null) { + parameters.clip = [{ + clip_skip: parseInt(clipSkip) + }] + } + + const sdUpscaleUpscaler = popOpt("sd upscale upscaler") + if (sdUpscaleUpscaler != null) { + const sdUpscaleOverlap = popOpt("sd upscale overlap") || "64" + parameters.sd_upscale = [{ + upscaler: sdUpscaleUpscaler, + overlap: parseInt(sdUpscaleOverlap) + }] + } + + for (const [extraNetworkType, extraNetworks] of Object.entries(infotext.extraNetworks)) { + for (const extraNetworkParams of extraNetworks) { + let strength; + switch (extraNetworkType.toLowerCase()) { + case "lora": + strength = parseFloat(extraNetworkParams.items[1]); + const lora: ComfyBoxStdGroupLoRA = { + model_name: extraNetworkParams.items[0], + strength_unet: strength, + strength_tenc: strength, + } + if (parameters.lora) + parameters.lora.push(lora) + else + parameters.lora = [lora] + break; + case "hypernet": + strength = parseFloat(extraNetworkParams.items[1]); + const hypernetwork: ComfyBoxStdGroupHypernetwork = { + model_name: extraNetworkParams.items[0], + strength + } + if (parameters.hypernetwork) + parameters.hypernetwork.push(hypernetwork) + else + parameters.hypernetwork = [hypernetwork] + break; + default: + break; + } + } + delete infotext.extraNetworks[extraNetworkType] + } + + let index = 1; + let found = infotext.extraParams[`addnet module ${index}`] + while (`addnet module ${index}` in infotext.extraParams) { + popOpt("addnet enabled") + const moduleName = popOpt(`addnet module ${index}`) + const modelName = popOpt(`addnet model ${index}`); + const weightA = popOpt(`addnet weight a ${index}`); + const weightB = popOpt(`addnet weight b ${index}`); + + if (moduleName == null || modelName == null || weightA == null || weightB == null) { + throw new Error(`Error parsing addnet model params: ${moduleName} ${modelName} ${weightA} ${weightB}`) + } + + if (moduleName !== "LoRA") { + throw new Error("Unknown AddNet model type " + moduleName) + } + + const [name, hash] = parseAddNetModelNameAndHash(modelName); + if (name == null || hash == null) { + throw new Error("Error parsing addnet model name: " + modelName); + } + + let shorthash = undefined + let shorthash_legacy = undefined + if (hash.length > 8) { + // new method using safetensors hash + shorthash = hash + } + else { + // old hash using webui's 0x10000 hashing method + shorthash_legacy = hash + } + + const lora: ComfyBoxStdGroupLoRA = { + model_name: name, + module_name: moduleName, + model_hashes: { + addnet_shorthash: shorthash, + addnet_shorthash_legacy: shorthash_legacy + }, + strength_unet: parseFloat(weightA), + strength_tenc: parseFloat(weightB), + } + if (parameters.lora) + parameters.lora.push(lora) + else + parameters.lora = [lora] + + index += 1; + found = infotext.extraParams[`addnet model ${index}`] + } + + for (const [key, value] of Object.entries(infotext.extraParams)) { + if (key.startsWith("addnet model ")) { + const index = key.replace("addnet module ", "") + // delete infotext.extraParams[key]; + } + } + + const prompt: ComfyBoxStdPrompt = { + prompt: { + metadata: { + version: 1, + created_with: "stable-diffusion-webui", + extra_data: {} + }, + parameters + } + } + + console.warn("Unhandled A1111 parameters:", infotext.extraParams, infotext.extraNetworks) + + return prompt +} diff --git a/src/lib/parseA1111.ts b/src/lib/parseA1111.ts new file mode 100644 index 0000000..0912d86 --- /dev/null +++ b/src/lib/parseA1111.ts @@ -0,0 +1,166 @@ +interface ExtraNetworkParams { + items: string[]; +} + +export type A1111ParsedInfotext = { + positive: string, + negative: string, + + steps: number, + cfgScale: number, + width: number, + height: number, + modelHash?: string, + modelName?: string, + batchSize?: number, + batchPos?: number, + sampler: string, + seed: number, + denoise?: number, + + extraNetworks: Record + extraParams: Record +} + +export type A1111ParsingError = { + error: string +} + +const reExtraNetworks = /<(\w+):([^>]+)>/g; +const reParam = /\s*([\w ]+):\s*("(?:\\"[^,]|\\"|\\|[^\"])+"|[^,]*)(?:,|$)/g; + +function parseExtraNetworks(prompt: string): [string, Record] { + const res: Record = {}; + + function found(_match: string, modelType: string, args: string): string { + if (!res[modelType]) { + res[modelType] = []; + } + + res[modelType].push({ items: args.split(":") }); + + return ""; + } + + prompt = prompt.replace(reExtraNetworks, found); + + return [prompt, res]; +} + +type A1111ParamHandler = string | ((prompt: A1111ParsedInfotext, value: string) => void); + +const wrapFloat = (name: string): ((p: A1111ParsedInfotext, v: string) => void) => { + return (p, v) => { + p[name] = parseFloat(v); + } +} + +const wrapInt = (name: string): A1111ParamHandler => { + return (p, v) => { + p[name] = parseInt(v); + } +} + +const handlers: Record = { + steps: wrapInt("steps"), + "cfg scale": wrapFloat("cfgScale"), + "size": (p, v) => { + const [widthStr, heightStr] = v.split("x") + p.width = parseInt(widthStr); + p.height = parseInt(heightStr); + }, + "model hash": "modelHash", + model: "modelName", + "batch size": wrapInt("batchSize"), + "batch pos": wrapInt("batchPos"), + sampler: "sampler", + seed: wrapInt("seed"), + "denoising strength": wrapFloat("denoise") +} + +/* + * Parses AUTOMATIC1111/stable-diffusion-webui format infotext into their raw parameters. + * + * Format is as follows: + * - Prompt text immediately starts at the start of the file, ending + * on the first line starting with "Negative prompt:" or "Steps:" + * - "Negative prompt:" is optional and might be omitted + * - Following "Steps:" are various sort-of-comma-separated values. + * Random characters can completely break parsing. Here be dragons. + */ +export default function parseA1111(infotext: string): A1111ParsedInfotext | A1111ParsingError { + let doneWithPrompt = false; + + let positive_ = "" + let negative = "" + + const lines = infotext.trim().split("\n") + let lastLineIdx = lines.findIndex(l => l.trim().indexOf("Steps: ") !== -1) + if (lastLineIdx === -1) { + return { error: "Steps: line not found" } + } + + for (let index = 0; index < lastLineIdx; index++) { + let line = lines[index].trim() + if (line.startsWith("Negative prompt:")) { + doneWithPrompt = true; + line = line.substring(16).trim(); + } + + if (doneWithPrompt) { + const addNewLine = negative != "" + negative += (addNewLine ? "\n" : "") + line + } + else { + const addNewLine = positive_ != "" + positive_ += (addNewLine ? "\n" : "") + line + } + } + + // webui doesn't apply extra networks in the negative prompt + let [positive, extraNetworks] = parseExtraNetworks(positive_) + + const extraParams: Record = {} + + let result: A1111ParsedInfotext = { + positive, + negative, + + // defaults taken from webui + width: 512, + height: 512, + steps: 20, + cfgScale: 7.0, + seed: -1, + sampler: "Euler", + + extraNetworks, + extraParams + } + + for (let index = lastLineIdx; index < lines.length; index++) { + const line = lines[index]; + for (let [_, key, value] of line.matchAll(reParam)) { + key = key.toLowerCase() + if (value[0] === '"' && value[value.length - 1] === '""') + value = value.substring(1, value.length - 1) + + const handler = handlers[key] + if (handler != null) { + if (value != null) { + if (typeof handler === "function") { + handler(result, value) + } + else { + (result as any)[handler] = value + } + } + } + else { + extraParams[key] = value + } + } + } + + return result; +} diff --git a/src/tests/convertA1111ToStdPromptTests.ts b/src/tests/convertA1111ToStdPromptTests.ts new file mode 100644 index 0000000..23e691a --- /dev/null +++ b/src/tests/convertA1111ToStdPromptTests.ts @@ -0,0 +1,234 @@ +import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt"; +import { expect } from 'vitest'; +import UnitTest from "./UnitTest"; +import type { A1111ParsedInfotext } from "$lib/parseA1111"; + +export default class convertA1111ToStdPromptTests extends UnitTest { + test__convertsBasic() { + const infotext: A1111ParsedInfotext = { + positive: "highest quality, masterpiece, best quality, masterpiece, asuka langley sitting cross legged on a chair", + negative: "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signature, watermark, username, blurry, artist name", + height: 512, + width: 512, + modelHash: "925997e9", + cfgScale: 12, + sampler: "Euler", + seed: 2870305590, + steps: 28, + extraNetworks: {}, + extraParams: { + "clip skip": "2", + "aesthetic embedding": "Belle", + "aesthetic lr": "0.0005", + "aesthetic slerp": "False", + "aesthetic slerp angle": "0.1", + "aesthetic steps": "15", + "aesthetic text": "", + "aesthetic text negative": "False", + "aesthetic weight": "0.9", + }, + } + + const converted = convertA1111ToStdPrompt(infotext); + + expect(converted).toEqual({ + prompt: { + metadata: { + version: 1, + created_with: "stable-diffusion-webui", + extra_data: {} + }, + parameters: { + checkpoint: [{ + model_hashes: { + a1111_shorthash: "925997e9", + } + }], + clip: [{ + clip_skip: 2, + }], + k_sampler: [{ + cfg_scale: 12, + denoise: 1, + sampler_name: "euler", + scheduler: "normal", + seed: 2870305590, + steps: 28 + }], + latent_image: [{ + width: 512, + height: 512, + }] + } + } + }) + } + + test__convertsExtraNetworks() { + const infotext: A1111ParsedInfotext = { + positive: "dreamlike fantasy landscape where everything is a shade of pink,\n dog ", + negative: "(worst quality:1.4), (low quality:1.4) , (monochrome:1.1)", + width: 640, + height: 512, + modelHash: "0f0eaaa61e", + modelName: "pastelmix-better-vae-fp16", + cfgScale: 12, + sampler: "DPM++ 2M Karras", + seed: 2416682767, + steps: 40, + denoise: 0.55, + extraNetworks: { + hypernet: [ + { items: ["zxcfc", "0.5", "baz", "quux"], }, + ], + lora: [ + { items: ["asdfg", "0.8", "foo", "bar"] }, + ], + }, + extraParams: { + "clip skip": "2", + "ensd": "31337", + "hires steps": "20", + "hires upscale": "2", + "hires upscaler": "Latent", + }, + } + + const converted = convertA1111ToStdPrompt(infotext); + + expect(converted).toEqual({ + prompt: { + metadata: { + version: 1, + created_with: "stable-diffusion-webui", + extra_data: {} + }, + parameters: { + checkpoint: [{ + model_name: "pastelmix-better-vae-fp16", + model_hashes: { + a1111_shorthash: "0f0eaaa61e", + } + }], + clip: [{ + clip_skip: 2, + }], + hypernetwork: [{ + model_name: "zxcfc", + strength: 0.5, + }], + lora: [{ + model_name: "asdfg", + strength_unet: 0.8, + strength_tenc: 0.8, + }], + k_sampler: [{ + cfg_scale: 12, + denoise: 0.55, + sampler_name: "dpmpp_2m", + scheduler: "karras", + seed: 2416682767, + steps: 40 + }], + latent_image: [{ + width: 640, + height: 512, + upscale_by: 2, + upscale_method: "Latent" + }] + } + } + }) + } + + test__convertsAdditionalNetworks() { + const infotext: A1111ParsedInfotext = { + positive: "1girl, pink hair", + negative: "(worst quality, low quality:1.4)", + width: 512, + height: 768, + modelHash: "0873291ac5", + modelName: "AbyssOrangeMix2_nsfw", + cfgScale: 6, + sampler: "DPM++ SDE Karras", + seed: 780207036, + steps: 20, + denoise: 0.2, + extraNetworks: {}, + extraParams: { + "addnet enabled": "True", + "addnet model 1": "ElysiaV3-000002(6d3eb064dcc1)", + "addnet model 2": "elfmorie2(a34cd9a8c3cc)", + "addnet module 1": "LoRA", + "addnet module 2": "LoRA", + "addnet weight a 1": "0.9", + "addnet weight a 2": "1", + "addnet weight b 1": "0.7", + "addnet weight b 2": "0.8", + "ensd": "31337", + "mask blur": "1", + "sd upscale overlap": "64", + "sd upscale upscaler": "4x_Valar_v1", + // XXX: just make sure it doesn't fall over for now + // this prompt format I swear... + "template": "1girl", + "negative template": "(worst quality", + } + } + + const converted = convertA1111ToStdPrompt(infotext) + + expect(converted).toEqual({ + prompt: { + metadata: { + version: 1, + created_with: "stable-diffusion-webui", + extra_data: {} + }, + parameters: { + checkpoint: [{ + model_name: "AbyssOrangeMix2_nsfw", + model_hashes: { + a1111_shorthash: "0873291ac5", + } + }], + lora: [{ + module_name: "LoRA", + model_name: "ElysiaV3-000002", + model_hashes: { + addnet_shorthash: "6d3eb064dcc1" + }, + strength_unet: 0.9, + strength_tenc: 0.7, + }, + { + module_name: "LoRA", + model_name: "elfmorie2", + model_hashes: { + addnet_shorthash: "a34cd9a8c3cc" + }, + strength_unet: 1, + strength_tenc: 0.8, + }], + k_sampler: [{ + cfg_scale: 6, + denoise: 0.2, + sampler_name: "dpmpp_sde", + scheduler: "karras", + seed: 780207036, + steps: 20 + }], + latent_image: [{ + width: 512, + height: 768, + mask_blur: 1 + }], + sd_upscale: [{ + upscaler: "4x_Valar_v1", + overlap: 64 + }] + } + } + }) + } +} diff --git a/src/tests/parseA1111Tests.ts b/src/tests/parseA1111Tests.ts new file mode 100644 index 0000000..3dd21da --- /dev/null +++ b/src/tests/parseA1111Tests.ts @@ -0,0 +1,158 @@ +import parseA1111 from "$lib/parseA1111"; +import { expect } from 'vitest'; +import UnitTest from "./UnitTest"; + +export default class parseA1111Tests extends UnitTest { + test__parsesBasic() { + const infotext = ` +highest quality, masterpiece, best quality, masterpiece, asuka langley sitting cross legged on a chair +Negative prompt: lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signature, watermark, username, blurry, artist name +Size: 512x512, Seed: 2870305590, Steps: 28, Sampler: Euler, CFG scale: 12, Clip skip: 2, Model hash: 925997e9, Aesthetic LR: 0.0005, Aesthetic text: , Aesthetic slerp: False, Aesthetic steps: 15, Aesthetic weight: 0.9, Aesthetic embedding: Belle, Aesthetic slerp angle: 0.1, Aesthetic text negative: False +` + + const parsed = parseA1111(infotext); + + expect(parsed).toEqual({ + positive: "highest quality, masterpiece, best quality, masterpiece, asuka langley sitting cross legged on a chair", + negative: "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signature, watermark, username, blurry, artist name", + height: 512, + width: 512, + modelHash: "925997e9", + cfgScale: 12, + sampler: "Euler", + seed: 2870305590, + steps: 28, + extraNetworks: {}, + extraParams: { + "clip skip": "2", + "aesthetic embedding": "Belle", + "aesthetic lr": "0.0005", + "aesthetic slerp": "False", + "aesthetic slerp angle": "0.1", + "aesthetic steps": "15", + "aesthetic text": "", + "aesthetic text negative": "False", + "aesthetic weight": "0.9", + }, + }) + } + + test__parsesExtraNetworks() { + const infotext = ` +dreamlike fantasy landscape where everything is a shade of pink, + dog +Negative prompt: (worst quality:1.4), (low quality:1.4) , (monochrome:1.1) +Steps: 40, Sampler: DPM++ 2M Karras, CFG scale: 12, Seed: 2416682767, Size: 640x512, Model hash: 0f0eaaa61e, Model: pastelmix-better-vae-fp16, Denoising strength: 0.55, Clip skip: 2, ENSD: 31337, Hires upscale: 2, Hires steps: 20, Hires upscaler: Latent +` + const parsed = parseA1111(infotext); + + expect(parsed).toEqual({ + positive: "dreamlike fantasy landscape where everything is a shade of pink,\n dog ", + negative: "(worst quality:1.4), (low quality:1.4) , (monochrome:1.1)", + width: 640, + height: 512, + modelHash: "0f0eaaa61e", + modelName: "pastelmix-better-vae-fp16", + cfgScale: 12, + sampler: "DPM++ 2M Karras", + seed: 2416682767, + steps: 40, + denoise: 0.55, + extraNetworks: { + hypernet: [ + { items: ["0.5", "baz", "quux"], }, + ], + lora: [ + { items: ["asdfg", "1", "foo", "bar"] }, + ], + }, + extraParams: { + "clip skip": "2", + "ensd": "31337", + "hires steps": "20", + "hires upscale": "2", + "hires upscaler": "Latent", + }, + }) + } + + test__parsesXYZGrid() { + const infotext = ` +1girl +Negative prompt: (worst quality, low quality:1.4) +Steps: 20, Sampler: DPM++ SDE Karras, CFG scale: 5, Seed: 1964718363, Size: 512x512, Model hash: 736a6f43c2, Denoising strength: 0.5, Clip skip: 2, Hires upscale: 1.75, Hires steps: 14, Hires upscaler: Latent (nearest-exact), Script: X/Y/Z plot, X Type: Prompt S/R, X Values: " , ,, , , ," +` + + const parsed = parseA1111(infotext); + + expect(parsed).toEqual({ + positive: "1girl", + negative: "(worst quality, low quality:1.4)", + width: 512, + height: 512, + modelHash: "736a6f43c2", + cfgScale: 5, + sampler: "DPM++ SDE Karras", + seed: 1964718363, + steps: 20, + denoise: 0.5, + extraNetworks: {}, + extraParams: { + "clip skip": "2", + "hires steps": "14", + "hires upscale": "1.75", + "hires upscaler": "Latent (nearest-exact)", + "script": "X/Y/Z plot", + "x type": "Prompt S/R", + "x values": '" , ,, , , ,"', + }, + }) + } + + test__parsesDynamicPromptsTemplates() { + const infotext = ` +1girl, pink hair +Negative prompt: (worst quality, low quality:1.4) +Steps: 20, Sampler: DPM++ SDE Karras, CFG scale: 6, Seed: 780207036, Size: 512x768, Model hash: 0873291ac5, Model: AbyssOrangeMix2_nsfw, Denoising strength: 0.2, ENSD: 31337, Mask blur: 1, SD upscale overlap: 64, SD upscale upscaler: 4x_Valar_v1, AddNet Enabled: True, AddNet Module 1: LoRA, AddNet Model 1: ElysiaV3-000002(6d3eb064dcc1), AddNet Weight A 1: 0.9, AddNet Weight B 1: 0.9, AddNet Module 2: LoRA, AddNet Model 2: elfmorie2(a34cd9a8c3cc), AddNet Weight A 2: 1, AddNet Weight B 2: 1 +Template: 1girl, __haircolor__ +Negative Template: (worst quality, low quality:1.4), __badprompt__ +` + + const parsed = parseA1111(infotext); + + expect(parsed).toEqual({ + positive: "1girl, pink hair", + negative: "(worst quality, low quality:1.4)", + width: 512, + height: 768, + modelHash: "0873291ac5", + modelName: "AbyssOrangeMix2_nsfw", + cfgScale: 6, + sampler: "DPM++ SDE Karras", + seed: 780207036, + steps: 20, + denoise: 0.2, + extraNetworks: {}, + extraParams: { + "addnet enabled": "True", + "addnet model 1": "ElysiaV3-000002(6d3eb064dcc1)", + "addnet model 2": "elfmorie2(a34cd9a8c3cc)", + "addnet module 1": "LoRA", + "addnet module 2": "LoRA", + "addnet weight a 1": "0.9", + "addnet weight a 2": "1", + "addnet weight b 1": "0.9", + "addnet weight b 2": "1", + "ensd": "31337", + "low quality": "1.4)", + "mask blur": "1", + "sd upscale overlap": "64", + "sd upscale upscaler": "4x_Valar_v1", + // XXX: just make sure it doesn't fall over for now + // this prompt format I swear... + "template": "1girl", + "negative template": "(worst quality", + }, + }) + } +} diff --git a/src/tests/testSuite.ts b/src/tests/testSuite.ts index b0dd6be..3d067bd 100644 --- a/src/tests/testSuite.ts +++ b/src/tests/testSuite.ts @@ -1,2 +1,4 @@ export { default as ComfyPromptSerializerTests } from "./ComfyPromptSerializerTests" export { default as ComfyGraphTests } from "./ComfyGraphTests" +export { default as parseA1111Tests } from "./parseA1111Tests" +export { default as convertA1111ToStdPromptTests } from "./convertA1111ToStdPromptTests" diff --git a/tsconfig.json b/tsconfig.json index 4f62860..1bbfae0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "resolveJsonModule": true, "allowJs": true, "checkJs": true, + "strict": true, "baseUrl": "./src", "paths": { "$lib": ["lib"], From 6aeaa91f795ebb91cde4aa85e7a74d8b72bd2b45 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Thu, 18 May 2023 20:02:59 -0500 Subject: [PATCH 03/14] Split normal/HR steps into two LatentImage/KSampler prompt entries --- src/lib/ComfyBoxStdPrompt.ts | 5 +-- src/lib/convertA1111ToStdPrompt.ts | 45 ++++++++++++++++++++--- src/tests/convertA1111ToStdPromptTests.ts | 14 ++++++- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/lib/ComfyBoxStdPrompt.ts b/src/lib/ComfyBoxStdPrompt.ts index de0c66a..af04ed3 100644 --- a/src/lib/ComfyBoxStdPrompt.ts +++ b/src/lib/ComfyBoxStdPrompt.ts @@ -39,17 +39,16 @@ const GroupKSampler = z.object({ sampler_name: z.string(), scheduler: z.string(), denoise: z.number().default(1.0) + type: z.enum(["empty", "image", "upscale"]).optional() }) export type ComfyBoxStdGroupKSampler = z.infer const GroupLatentImage = z.object({ width: z.number(), height: z.number(), - type: z.enum(["empty", "image", "image_upscale"]).optional(), + type: z.enum(["empty", "image", "upscale"]).optional(), upscale_method: z.string().optional(), upscale_by: z.number().optional(), - upscale_width: z.number().optional(), - upscale_height: z.number().optional(), crop: z.string().optional(), mask_blur: z.number().optional(), batch_count: z.number().default(1).optional(), diff --git a/src/lib/convertA1111ToStdPrompt.ts b/src/lib/convertA1111ToStdPrompt.ts index 8726a5e..82bc03a 100644 --- a/src/lib/convertA1111ToStdPrompt.ts +++ b/src/lib/convertA1111ToStdPrompt.ts @@ -35,7 +35,9 @@ export default function convertA1111ToStdPrompt(infotext: A1111ParsedInfotext): const hrUp = popOpt("hires upscale"); const hrSz = popOpt("hires resize"); + let hrScaleBy = hrUp ? parseFloat(hrUp) : undefined; let hrMethod = popOpt("hires upscaler"); + let hrSteps = popOpt("hires steps"); let hrWidth = undefined let hrHeight = undefined if (hrSz) { @@ -51,10 +53,7 @@ export default function convertA1111ToStdPrompt(infotext: A1111ParsedInfotext): const latent_image: ComfyBoxStdGroupLatentImage = { width: infotext.width, height: infotext.height, - upscale_method: hrMethod, - upscale_by: hrUp ? parseFloat(hrUp) : undefined, - upscale_width: hrWidth, - upscale_height: hrHeight, + // type: "empty", // detect txt2img??? batch_count: infotext.batchSize, batch_pos: infotext.batchPos, } @@ -65,18 +64,54 @@ export default function convertA1111ToStdPrompt(infotext: A1111ParsedInfotext): parameters.latent_image = [latent_image]; + if (hrMethod != null) { + let uw, uh; + if (hrScaleBy) { + uw = infotext.width * hrScaleBy; + uh = infotext.height * hrScaleBy; + } else { + if (hrWidth == null || hrHeight == null) + throw new Error("Highres prompt didn't have width/height!") + uw = +hrWidth; + uh = +hrHeight; + } + const hr_image: ComfyBoxStdGroupLatentImage = { + type: "upscale", + width: uw, + height: uh, + upscale_by: hrScaleBy, + batch_count: infotext.batchSize, + batch_pos: infotext.batchPos, + upscale_method: hrMethod + } + parameters.latent_image.push(hr_image) + } + const [sampler_name, scheduler] = getSamplerAndScheduler(infotext.sampler) const k_sampler: ComfyBoxStdGroupKSampler = { steps: infotext.steps, seed: infotext.seed, cfg_scale: infotext.cfgScale, - denoise: infotext.denoise || 1.0, + denoise: hrMethod != null ? 1.0 : infotext.denoise || 1.0, // detect img2img??? sampler_name, scheduler, } parameters.k_sampler = [k_sampler]; + if (hrMethod != null) { + const k_sampler_hr: ComfyBoxStdGroupKSampler = { + type: "upscale", + steps: hrSteps != null ? parseInt(hrSteps) : infotext.steps, + seed: infotext.seed, + cfg_scale: infotext.cfgScale, + denoise: infotext.denoise || 1.0, + sampler_name, + scheduler, + } + parameters.k_sampler.push(k_sampler_hr) + } + if (infotext.modelHash || infotext.modelName) { const checkpoint: ComfyBoxStdGroupCheckpoint = { model_name: infotext.modelName, diff --git a/src/tests/convertA1111ToStdPromptTests.ts b/src/tests/convertA1111ToStdPromptTests.ts index 23e691a..5ef0b9a 100644 --- a/src/tests/convertA1111ToStdPromptTests.ts +++ b/src/tests/convertA1111ToStdPromptTests.ts @@ -124,15 +124,27 @@ export default class convertA1111ToStdPromptTests extends UnitTest { }], k_sampler: [{ cfg_scale: 12, - denoise: 0.55, + denoise: 1, sampler_name: "dpmpp_2m", scheduler: "karras", seed: 2416682767, steps: 40 + }, { + type: "upscale", + cfg_scale: 12, + denoise: 0.55, + sampler_name: "dpmpp_2m", + scheduler: "karras", + seed: 2416682767, + steps: 20 }], latent_image: [{ width: 640, height: 512, + }, { + type: "upscale", + width: 1280, + height: 1024, upscale_by: 2, upscale_method: "Latent" }] From 3eb656d0df661f3be72b1f6204bc2dc60a920ac4 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Thu, 18 May 2023 20:26:24 -0500 Subject: [PATCH 04/14] forgot to convert prompt --- src/lib/ComfyBoxStdPrompt.ts | 19 +++++++++++++++++++ src/lib/convertA1111ToStdPrompt.ts | 14 +++++++------- src/tests/convertA1111ToStdPromptTests.ts | 12 ++++++++++++ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/lib/ComfyBoxStdPrompt.ts b/src/lib/ComfyBoxStdPrompt.ts index af04ed3..a5ff3d8 100644 --- a/src/lib/ComfyBoxStdPrompt.ts +++ b/src/lib/ComfyBoxStdPrompt.ts @@ -108,6 +108,18 @@ const GroupDynamicThresholding = z.object({ }) export type ComfyBoxStdGroupDynamicThresholding = z.infer +const GroupAestheticEmbedding = z.object({ + model_name: z.string(), + lr: z.number(), + slerp: z.boolean(), + slerp_angle: z.number(), + steps: z.number(), + positive: z.string(), + negative: z.string(), + weight: z.number(), +}) +export type ComfyBoxStdGroupAestheticEmbedding = z.infer + const group = (s: ZodTypeAny) => z.optional(z.array(s).nonempty()); const Parameters = z.object({ @@ -151,4 +163,11 @@ const ComfyBoxStdPrompt = z.object({ }) export default ComfyBoxStdPrompt + +/* + * A standardized Stable Diffusion prompt and parameter format, to be used with + * an encompassing workflow. Aims to encompass an arbitrary number of parameter + * counts and types, so that most ComfyUI workflows can have parts of their + * prompts transferred between each other. + */ export type ComfyBoxStdPrompt = z.infer diff --git a/src/lib/convertA1111ToStdPrompt.ts b/src/lib/convertA1111ToStdPrompt.ts index 82bc03a..37a43e5 100644 --- a/src/lib/convertA1111ToStdPrompt.ts +++ b/src/lib/convertA1111ToStdPrompt.ts @@ -33,6 +33,11 @@ export default function convertA1111ToStdPrompt(infotext: A1111ParsedInfotext): const parameters: ComfyBoxStdParameters = {} + parameters.prompt = [{ + positive: infotext.positive, + negative: infotext.negative, + }] + const hrUp = popOpt("hires upscale"); const hrSz = popOpt("hires resize"); let hrScaleBy = hrUp ? parseFloat(hrUp) : undefined; @@ -138,6 +143,8 @@ export default function convertA1111ToStdPrompt(infotext: A1111ParsedInfotext): }] } + // TODO ControlNet + for (const [extraNetworkType, extraNetworks] of Object.entries(infotext.extraNetworks)) { for (const extraNetworkParams of extraNetworks) { let strength; @@ -224,13 +231,6 @@ export default function convertA1111ToStdPrompt(infotext: A1111ParsedInfotext): found = infotext.extraParams[`addnet model ${index}`] } - for (const [key, value] of Object.entries(infotext.extraParams)) { - if (key.startsWith("addnet model ")) { - const index = key.replace("addnet module ", "") - // delete infotext.extraParams[key]; - } - } - const prompt: ComfyBoxStdPrompt = { prompt: { metadata: { diff --git a/src/tests/convertA1111ToStdPromptTests.ts b/src/tests/convertA1111ToStdPromptTests.ts index 5ef0b9a..e1c63e8 100644 --- a/src/tests/convertA1111ToStdPromptTests.ts +++ b/src/tests/convertA1111ToStdPromptTests.ts @@ -44,6 +44,10 @@ export default class convertA1111ToStdPromptTests extends UnitTest { a1111_shorthash: "925997e9", } }], + prompt: [{ + positive: "highest quality, masterpiece, best quality, masterpiece, asuka langley sitting cross legged on a chair", + negative: "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signature, watermark, username, blurry, artist name" + }], clip: [{ clip_skip: 2, }], @@ -110,6 +114,10 @@ export default class convertA1111ToStdPromptTests extends UnitTest { a1111_shorthash: "0f0eaaa61e", } }], + prompt: [{ + positive: "dreamlike fantasy landscape where everything is a shade of pink,\n dog ", + negative: "(worst quality:1.4), (low quality:1.4) , (monochrome:1.1)" + }], clip: [{ clip_skip: 2, }], @@ -204,6 +212,10 @@ export default class convertA1111ToStdPromptTests extends UnitTest { a1111_shorthash: "0873291ac5", } }], + prompt: [{ + positive: "1girl, pink hair", + negative: "(worst quality, low quality:1.4)", + }], lora: [{ module_name: "LoRA", model_name: "ElysiaV3-000002", From 74a1b5c636416fd7408296e78d6e155fe277c734 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Thu, 18 May 2023 23:06:07 -0500 Subject: [PATCH 05/14] View parsed A1111 prompt info --- package.json | 1 + pnpm-lock.yaml | 11 ++ src/lib/ComfyBoxStdPrompt.ts | 41 +++++- src/lib/components/A1111PromptDisplay.svelte | 114 ++++++++++++++++ src/lib/components/ComfyApp.svelte | 76 ++++++----- src/lib/components/ComfyApp.ts | 111 +++++++--------- src/lib/components/ComfyUIPane.svelte | 2 + src/lib/components/DropZone.svelte | 78 +++++++++++ src/lib/components/Modal.svelte | 1 + src/lib/convertA1111ToStdPrompt.ts | 131 ++++++++++++++++--- src/tests/convertA1111ToStdPromptTests.ts | 13 +- tsconfig.json | 2 +- 12 files changed, 457 insertions(+), 124 deletions(-) create mode 100644 src/lib/components/A1111PromptDisplay.svelte create mode 100644 src/lib/components/DropZone.svelte diff --git a/package.json b/package.json index 9eafe53..aef40d0 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@litegraph-ts/tsconfig": "workspace:*", "@sveltejs/vite-plugin-svelte": "^2.1.1", "@tsconfig/svelte": "^4.0.1", + "@zerodevx/svelte-json-view": "^1.0.5", "events": "^3.3.0", "framework7": "^8.0.3", "framework7-svelte": "^8.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc388df..d1bd089 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ importers: '@tsconfig/svelte': specifier: ^4.0.1 version: 4.0.1 + '@zerodevx/svelte-json-view': + specifier: ^1.0.5 + version: 1.0.5(svelte@3.58.0) events: specifier: ^3.3.0 version: 3.3.0 @@ -3375,6 +3378,14 @@ packages: pretty-format: 27.5.1 dev: false + /@zerodevx/svelte-json-view@1.0.5(svelte@3.58.0): + resolution: {integrity: sha512-oQDI9v0dJEte6PYVDVjLOjU58AOoWLYRXjghKggFpZXrglWJJqoMeDe14Jrd0cs6NPcPogT/aR/LtkuW2Z1GkQ==} + peerDependencies: + svelte: ^3.55.1 + dependencies: + svelte: 3.58.0 + dev: false + /@zerodevx/svelte-toast@0.9.3(svelte@3.58.0): resolution: {integrity: sha512-VPKWR4A9y01fyXRscu9HiTj7tV2hFrpRKZvGwMmaPXfHIXR1D9+NNsz0HXcQ7qZ0C5UaHS3n9uNtPtIcAXT7RQ==} peerDependencies: diff --git a/src/lib/ComfyBoxStdPrompt.ts b/src/lib/ComfyBoxStdPrompt.ts index a5ff3d8..ac44ed7 100644 --- a/src/lib/ComfyBoxStdPrompt.ts +++ b/src/lib/ComfyBoxStdPrompt.ts @@ -38,7 +38,7 @@ const GroupKSampler = z.object({ steps: z.number(), sampler_name: z.string(), scheduler: z.string(), - denoise: z.number().default(1.0) + denoise: z.number().default(1.0), type: z.enum(["empty", "image", "upscale"]).optional() }) export type ComfyBoxStdGroupKSampler = z.infer @@ -62,6 +62,12 @@ const GroupSDUpscale = z.object({ }) export type ComfyBoxStdGroupSDUpscale = z.infer +const GroupSelfAttentionGuidance = z.object({ + guidance_scale: z.number(), + mask_threshold: z.number(), +}) +export type ComfyBoxStdGroupSelfAttentionGuidance = z.infer + const GroupHypernetwork = z.object({ model_name: z.string(), model_hashes: ModelHashes.optional(), @@ -102,7 +108,7 @@ const GroupDynamicThresholding = z.object({ mimic_scale: z.number(), threshold_percentile: z.number(), mimic_mode: z.string(), - mimic_scale_min: z.number(), + mimic_scale_minimum: z.number(), cfg_mode: z.string(), cfg_scale_minimum: z.number() }) @@ -120,6 +126,23 @@ const GroupAestheticEmbedding = z.object({ }) export type ComfyBoxStdGroupAestheticEmbedding = z.infer +const GroupDDetailer = z.object({ + positive_prompt: z.string(), + negative_prompt: z.string(), + bitwise: z.string(), + model: z.string().optional(), + model_hashes: ModelHashes.optional(), + conf: z.number(), + mask_blur: z.number(), + denoise: z.number(), + dilation: z.number(), + offset_x: z.number(), + offset_y: z.number(), + inpaint_full: z.number(), + inpaint_padding: z.number(), +}) +export type ComfyBoxStdGroupDDetailer = z.infer + const group = (s: ZodTypeAny) => z.optional(z.array(s).nonempty()); const Parameters = z.object({ @@ -133,7 +156,9 @@ const Parameters = z.object({ hypernetwork: group(GroupHypernetwork), lora: group(GroupLoRA), control_net: group(GroupControlNet), - dynamic_thresholding: group(GroupDynamicThresholding) + dynamic_thresholding: group(GroupDynamicThresholding), + self_attention_guidance: group(GroupSelfAttentionGuidance), + ddetailer: group(GroupDDetailer) }).partial() export type ComfyBoxStdParameters = z.infer @@ -141,14 +166,19 @@ const ComfyBoxExtraData = z.object({ workflows: z.array(z.string()) }) +const A1111ExtraData = z.object({ + params: z.any() +}) + const ExtraData = z.object({ - comfybox: ComfyBoxExtraData.optional() + comfybox: ComfyBoxExtraData.optional(), + a1111: A1111ExtraData.optional() }) const Metadata = z.object({ - version: z.number(), created_with: z.string(), author: z.string().optional(), + app_version: z.string().optional(), commit_hash: z.string().optional(), extra_data: ExtraData }) @@ -159,6 +189,7 @@ const Prompt = z.object({ }) const ComfyBoxStdPrompt = z.object({ + version: z.number(), prompt: Prompt, }) diff --git a/src/lib/components/A1111PromptDisplay.svelte b/src/lib/components/A1111PromptDisplay.svelte new file mode 100644 index 0000000..134ff5b --- /dev/null +++ b/src/lib/components/A1111PromptDisplay.svelte @@ -0,0 +1,114 @@ + + +{#if prompt != null} +
+
+ + + +
+ {#if a1111} + {#if Object.keys(a1111.extraParams).length > 0} + + Unused Parameters +
+ +
+
+ {/if} + {/if} + + Converted Prompt +
+ +
+
+
+
+ + + +
+{/if} + + diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index 7ded4ef..f99acbb 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -5,13 +5,12 @@ import { Button } from "@gradio/button"; import { BlockTitle } from "@gradio/atoms"; import ComfyUIPane from "./ComfyUIPane.svelte"; - import ComfyApp, { type SerializedAppState } from "./ComfyApp"; + import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp"; import { Checkbox, TextBox } from "@gradio/form" import uiState from "$lib/stores/uiState"; import layoutState from "$lib/stores/layoutState"; import selectionState from "$lib/stores/selectionState"; import { ImageViewer } from "$lib/ImageViewer"; - import type { ComfyAPIStatus } from "$lib/api"; import { SvelteToast, toast } from '@zerodevx/svelte-toast' import { LGraph } from "@litegraph-ts/core"; @@ -20,14 +19,18 @@ import ComfyProperties from "./ComfyProperties.svelte"; import queueState from "$lib/stores/queueState"; import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte"; - import ComfyGraphView from "./ComfyGraphView.svelte"; - import { download, jsonToJsObject } from "$lib/utils"; - import notify from "$lib/notify"; + import ComfyGraphView from "./ComfyGraphView.svelte"; + import { download, jsonToJsObject } from "$lib/utils"; + import notify from "$lib/notify"; + import Modal from "./Modal.svelte"; + import ComfyBoxStdPrompt from "$lib/ComfyBoxStdPrompt"; + import A1111PromptDisplay from "./A1111PromptDisplay.svelte"; + import type { A1111ParsedInfotext } from "$lib/parseA1111"; export let app: ComfyApp = undefined; - let queue: ComfyQueue = undefined; + let alreadySetup: Writable = writable(false); + let a1111Prompt: Writable = writable(null); let mainElem: HTMLDivElement; - let uiPane: ComfyUIPane = undefined; let props: ComfyProperties = undefined; let containerElem: HTMLDivElement; let resizeTimeout: NodeJS.Timeout | null; @@ -44,6 +47,11 @@ } } + $: if(app) { + alreadySetup = app.alreadySetup; + a1111Prompt = app.a1111Prompt; + } + function refreshView(event?: Event) { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(app.resizeCanvas.bind(app), 250); @@ -188,6 +196,10 @@ else { document.getElementById("app-root").classList.remove("dark") } + + let showModal: boolean = false; + + $: showModal = $a1111Prompt != null @@ -196,8 +208,19 @@ {/if} + ($a1111Prompt = null)}> +
+

A1111 Prompt Details

+
+ +
+ +
+
+
-
@@ -208,7 +231,7 @@ - + @@ -217,7 +240,7 @@ @@ -225,35 +248,35 @@
{#if $layoutState.attrs.queuePromptButtonName != ""} - {/if} - - - - - - - - - {#if !disabled}
Date: Sat, 20 May 2023 09:38:32 -0500 Subject: [PATCH 09/14] Move dropzone to queue window --- src/lib/components/ComfyApp.svelte | 2 +- src/lib/components/ComfyQueue.svelte | 6 +++++- src/lib/components/ComfyUIPane.svelte | 2 -- src/lib/components/DropZone.svelte | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index f99acbb..e3f11e2 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -240,7 +240,7 @@ diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte index b10259f..edaaaee 100644 --- a/src/lib/components/ComfyQueue.svelte +++ b/src/lib/components/ComfyQueue.svelte @@ -11,7 +11,10 @@ import { Button } from "@gradio/button"; import type ComfyApp from "./ComfyApp"; import { tick } from "svelte"; - import Modal from "./Modal.svelte"; + import Modal from "./Modal.svelte"; + import DropZone from "./DropZone.svelte"; + + export let app: ComfyApp; let queuePending: Writable | null = null; let queueRunning: Writable | null = null; @@ -197,6 +200,7 @@
+
{#if _entries.length > 0} {#each _entries as entry} diff --git a/src/lib/components/ComfyUIPane.svelte b/src/lib/components/ComfyUIPane.svelte index 92975bd..b4a2bc8 100644 --- a/src/lib/components/ComfyUIPane.svelte +++ b/src/lib/components/ComfyUIPane.svelte @@ -9,7 +9,6 @@ import layoutState, { type ContainerLayout, type DragItem, type IDragItem } from "$lib/stores/layoutState"; import uiState from "$lib/stores/uiState"; import selectionState from "$lib/stores/selectionState"; - import DropZone from "./DropZone.svelte"; import Menu from './menu/Menu.svelte'; import MenuOption from './menu/MenuOption.svelte'; @@ -151,7 +150,6 @@
-
diff --git a/src/lib/components/DropZone.svelte b/src/lib/components/DropZone.svelte index d23c517..9c949b3 100644 --- a/src/lib/components/DropZone.svelte +++ b/src/lib/components/DropZone.svelte @@ -50,7 +50,7 @@ } - + {#if !disabled}
Date: Sat, 20 May 2023 09:39:28 -0500 Subject: [PATCH 10/14] Small performance boost for dragging within containers --- src/lib/components/AccordionContainer.svelte | 4 ++-- src/lib/components/BlockContainer.svelte | 8 ++++---- src/lib/components/TabsContainer.svelte | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib/components/AccordionContainer.svelte b/src/lib/components/AccordionContainer.svelte index ca86d8f..279aafe 100644 --- a/src/lib/components/AccordionContainer.svelte +++ b/src/lib/components/AccordionContainer.svelte @@ -25,7 +25,7 @@ export let isMobile: boolean = false; let isOpen: Writable | null = null; - let children: IDragItem[] | null = null; + let children: IDragItem[] = []; const flipDurationMs = 100; let selectedIndex: number = 0; @@ -61,7 +61,7 @@ } -{#if container && Array.isArray(children)} +{#if container} {@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
| null = null; - let children: IDragItem[] | null = null; + let children: IDragItem[] = []; const flipDurationMs = 100; $: if (container) { @@ -35,12 +35,12 @@ } else { container = null; - children = null; + children = []; attrsChanged = null; } } else { - children = null; + children = []; attrsChanged = null } @@ -55,7 +55,7 @@ }; -{#if container && Array.isArray(children)} +{#if container} {@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
| null = null; - let children: IDragItem[] | null = null; + let children: IDragItem[] = []; const flipDurationMs = 100; let selectedIndex: number = 0; @@ -35,7 +35,7 @@ attrsChanged = container.attrsChanged } else { - children = null; + children = []; attrsChanged = null } @@ -68,7 +68,7 @@ } -{#if container && Array.isArray(children)} +{#if container} {@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
Date: Sat, 20 May 2023 10:39:20 -0500 Subject: [PATCH 11/14] More efficient loading for combo widget --- litegraph | 2 +- src/lib/nodes/widgets/ComfyComboNode.ts | 4 +- src/lib/widgets/ComboWidget.svelte | 145 ++++++++++++------------ 3 files changed, 76 insertions(+), 75 deletions(-) diff --git a/litegraph b/litegraph index db6a916..e326ab3 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit db6a9167575db269524564d5527c512a06c0f8d2 +Subproject commit e326ab331e39b4c4c43f3f10666e384781776e3f diff --git a/src/lib/nodes/widgets/ComfyComboNode.ts b/src/lib/nodes/widgets/ComfyComboNode.ts index bb8b5f9..de6fedd 100644 --- a/src/lib/nodes/widgets/ComfyComboNode.ts +++ b/src/lib/nodes/widgets/ComfyComboNode.ts @@ -40,13 +40,13 @@ export default class ComfyComboNode extends ComfyWidgetNode { // Wait until the initial graph load for combo to be valid. firstLoad: Writable; lightUp: Writable; - valuesForCombo: Writable; // Changed when the combo box has values. + valuesForCombo: Writable; // Changed when the combo box has values. constructor(name?: string) { super(name, "A") this.firstLoad = writable(false) this.lightUp = writable(true) - this.valuesForCombo = writable(null) + this.valuesForCombo = writable([]) } override onPropertyChanged(property: any, value: any) { diff --git a/src/lib/widgets/ComboWidget.svelte b/src/lib/widgets/ComboWidget.svelte index ec384a0..cb28b69 100644 --- a/src/lib/widgets/ComboWidget.svelte +++ b/src/lib/widgets/ComboWidget.svelte @@ -11,10 +11,10 @@ export let widget: WidgetLayout | null = null; export let isMobile: boolean = false; let node: ComfyComboNode | null = null; - let nodeValue: Writable | null = null; + let nodeValue: Writable = writable(""); let propsChanged: Writable | null = null; let lightUp: Writable = writable(false); - let valuesForCombo: Writable | null = null; + let valuesForCombo: Writable = writable([]) let lastConfigured: any = null; let option: any = null; @@ -132,77 +132,69 @@
- {#key $valuesForCombo} - {#if node !== null && nodeValue !== null} - {#if $valuesForCombo == null} - Loading... - {:else} - - {/if} +