Image compare widget
This commit is contained in:
@@ -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
7
pnpm-lock.yaml
generated
@@ -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==}
|
||||
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
32
src/lib/components/ImageComparison.svelte
Normal file
32
src/lib/components/ImageComparison.svelte
Normal 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>
|
||||
@@ -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"
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
80
src/lib/widgets/ImageCompareWidget.svelte
Normal file
80
src/lib/widgets/ImageCompareWidget.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user