Image compare widget

This commit is contained in:
space-nuko
2023-05-11 00:14:25 -05:00
parent 0013833b16
commit bc4128f07a
8 changed files with 242 additions and 1 deletions

View File

@@ -59,6 +59,7 @@
"events": "^3.3.0",
"framework7": "^8.0.3",
"framework7-svelte": "^8.0.3",
"img-comparison-slider": "^8.0.0",
"pollen-css": "^4.6.2",
"radix-icons-svelte": "^1.2.1",
"svelte-preprocess": "^5.0.3",

7
pnpm-lock.yaml generated
View File

@@ -79,6 +79,9 @@ importers:
framework7-svelte:
specifier: ^8.0.3
version: 8.0.3
img-comparison-slider:
specifier: ^8.0.0
version: 8.0.0
pollen-css:
specifier: ^4.6.2
version: 4.6.2
@@ -4189,6 +4192,10 @@ packages:
engines: {node: '>= 4'}
dev: true
/img-comparison-slider@8.0.0:
resolution: {integrity: sha512-ZOKkdN/+W/U/2LFEwrZuxRVbIwQK1GyEKhTETfsy55/bmBoNfM81MnQsc1j81Q50dkwTKjuecicsnp3O7lBRqQ==}
dev: false
/immutable@4.3.0:
resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==}

View File

@@ -119,6 +119,12 @@ export default class ComfyAPI extends EventTarget {
case "executed":
this.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
break;
case "execution_cached":
this.dispatchEvent(new CustomEvent("execution_cached", { detail: msg.data }));
break;
case "execution_error":
this.dispatchEvent(new CustomEvent("execution_error", { detail: msg.data }));
break;
default:
if (this.registered.has(msg.type)) {
this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));

View File

@@ -352,6 +352,15 @@ export default class ComfyApp {
}
});
this.api.addEventListener("execution_cached", ({ detail }: CustomEvent) => {
// TODO detail.nodes
});
this.api.addEventListener("execution_error", ({ detail }: CustomEvent) => {
queueState.update(s => { s.progress = null; s.runningNodeId = null; return s; })
notify(`Execution error: ${detail.message}`, { type: "error", timeout: 10000 })
});
this.api.init();
}
@@ -447,6 +456,11 @@ export default class ComfyApp {
state = structuredClone(blankGraph)
}
await this.deserialize(state)
uiState.update(s => {
s.uiUnlocked = true;
s.uiEditMode = "widgets";
return s;
})
}
/**

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import 'img-comparison-slider';
export let value: number = 50;
export let hover: boolean = false;
export let direction: "horizontal" | "vertical" = "horizontal";
export let nonce: string | null = null;
export let keyboard: "enabled" | "disabled" = "enabled";
export let handle: boolean = false;
</script>
<img-comparison-slider {value} {hover} {direction} {nonce} {keyboard} {handle} {...$$restProps}>
<slot/>
</img-comparison-slider>
<style>
img-comparison-slider {
visibility: hidden;
}
img-comparison-slider [slot='second'] {
display: none;
}
img-comparison-slider.rendered {
visibility: inherit;
}
img-comparison-slider.rendered [slot='second'] {
display: unset;
}
</style>

View File

@@ -17,6 +17,7 @@ import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
import RadioWidget from "$lib/widgets/RadioWidget.svelte";
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
import ImageCompareWidget from "$lib/widgets/ImageCompareWidget.svelte";
export type AutoConfigOptions = {
includeProperties?: Set<string> | null,
@@ -963,3 +964,92 @@ LiteGraph.registerNodeType({
desc: "Widget that lets you upload images into ComfyUI's input folder",
type: "ui/image_upload"
})
export interface ComfyImageCompareNodeProperties extends ComfyWidgetProperties {
}
export type FileNameOrGalleryData = string | GalleryOutputEntry;
export type ImageCompareData = [FileNameOrGalleryData, FileNameOrGalleryData]
export class ComfyImageCompareNode extends ComfyWidgetNode<ImageCompareData> {
override properties: ComfyImageCompareNodeProperties = {
defaultValue: [],
tags: [],
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "store", type: BuiltInSlotType.ACTION },
{ name: "left_image", type: "string" },
{ name: "right_image", type: "string" },
],
outputs: [
]
}
override svelteComponentType = ImageCompareWidget;
override defaultValue: ImageCompareData = ["", ""];
override outputIndex = null;
override changedIndex = 3;
override storeActionName = "store";
override saveUserState = false;
constructor(name?: string) {
super(name, ["", ""])
}
override onExecute() {
const valueA = this.getInputData(1)
const valueB = this.getInputData(2)
let current = get(this.value)
let changed = false;
if (valueA != null && current[0] != valueA) {
current[0] = valueA
changed = true;
}
if (valueB != null && current[1] != valueB) {
current[1] = valueB
changed = true;
}
if (changed)
this.value.set(current)
}
override parseValue(value: any): ImageCompareData {
if (value == null) {
return ["", ""]
}
else if (typeof value === "string" && value !== "") { // Single filename
const prevValue = get(this.value)
prevValue.push(value)
if (prevValue.length > 2)
prevValue.splice(0, 1)
return prevValue as ImageCompareData
}
else if (typeof value === "object" && "images" in value && value.images.length > 0) {
const output = value as GalleryOutput
const prevValue = get(this.value)
prevValue.push(output.images[0].filename)
if (prevValue.length > 2)
prevValue.splice(0, 1)
return prevValue as ImageCompareData
}
else if (Array.isArray(value) && typeof value[0] === "string" && typeof value[1] === "string") {
return value as ImageCompareData
}
else {
return ["", ""]
}
}
override formatValue(value: GradioFileData[]): string {
return `Images: ${value?.length || 0}`
}
}
LiteGraph.registerNodeType({
class: ComfyImageCompareNode,
title: "UI.ImageCompare",
desc: "Widget that lets you compare two images",
type: "ui/image_compare"
})

View File

@@ -123,7 +123,6 @@ export const debounce = (callback: Function, wait = 250) => {
export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileData[] {
return output.images.map(r => {
// TODO configure backend URL
const url = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams(r)
const fileData: GradioFileData = {
@@ -136,6 +135,18 @@ export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileDat
});
}
export function convertFilenameToComfyURL(filename: string,
subfolder: string = "",
type: "input" | "output" | "temp" = "output"): string {
const params = new URLSearchParams({
filename,
subfolder,
type
})
const url = `http://${location.hostname}:8188` // TODO make configurable
return url + "/view?" + params
}
export function jsonToJsObject(json: string): string {
// Try to parse, to see if it's real JSON
JSON.parse(json);

View File

@@ -0,0 +1,80 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms";
import ImageComparison from "$lib/components/ImageComparison.svelte";
import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
import type { SelectData } from "@gradio/utils";
import type { ComfyImageCompareNode, ImageCompareData } from "$lib/nodes/ComfyWidgetNodes";
import { convertFilenameToComfyURL } from "$lib/utils";
import { TabItem, Tabs } from "@gradio/tabs";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyImageCompareNode | null = null;
let nodeValue: Writable<ImageCompareData> | null = null;
let attrsChanged: Writable<number> | null = null;
let leftUrl: string = ""
let rightUrl: string = ""
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyImageCompareNode
nodeValue = node.value;
attrsChanged = widget.attrsChanged;
}
};
const urlPattern = /^((http|https|ftp):\/\/)/;
$: updateUrls($nodeValue);
function updateUrls(value: ImageCompareData) {
leftUrl = ""
rightUrl = ""
console.warn("UPD", value)
if (typeof value[0] === "string") {
if (urlPattern.test(value[0]))
leftUrl = value[0]
else
leftUrl = convertFilenameToComfyURL(value[0])
}
if (typeof value[1] === "string") {
if (urlPattern.test(value[1]))
rightUrl = value[1]
else
rightUrl = convertFilenameToComfyURL(value[1])
}
}
</script>
<div class="wrapper comfy-compare-widget">
<Block>
<Tabs elem_classes={["gradio-tabs"]}>
<TabItem name="Slider">
<ImageComparison>
{#if leftUrl && leftUrl != ""}
{@const props = { slot: "first" }}
<img {...props} alt="Left" src={leftUrl} />
{/if}
{#if rightUrl && leftUrl != ""}
{@const props = { slot: "second" }}
<img {...props} alt="Right" src={rightUrl} />
{/if}
</ImageComparison>
</TabItem>
</Tabs>
</Block>
</div>
<style lang="scss">
.comfy-compare-widget {
max-width: 40rem;
img {
width: 100%;
}
}
</style>