Refactor image upload widget

This commit is contained in:
space-nuko
2023-05-13 13:28:34 -05:00
parent ea8502521b
commit 4d90623505
5 changed files with 133 additions and 58 deletions

View File

@@ -7,10 +7,10 @@
import UploadText from "$lib/components/gradio/app/UploadText.svelte";
import { tick } from "svelte";
import notify from "$lib/notify";
import type { ComfyUploadImageAPIResponse } from "$lib/utils";
import { convertComfyOutputToComfyURL, type ComfyUploadImageAPIResponse, converGradioFileDataToComfyURL } from "$lib/utils";
import type { GalleryOutputEntry, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
export let value: GradioFileData[] | null = null;
export let isMobile: boolean = false;
export let value: GalleryOutputEntry[] | null = null;
export let imgWidth: number = 0;
export let imgHeight: number = 0;
export let imgElem: HTMLImageElement | null = null
@@ -21,19 +21,23 @@
// let propsChanged: Writable<number> | null = null;
let dragging = false;
let pending_upload = false;
let old_value: Array<GradioFileData> | null = null;
let old_value: GradioFileData[] | null = null;
let _value: GradioFileData[] | null = null;
const root = "comf"
const root_url = "https//ComfyUI!"
const dispatch = createEventDispatcher<{
change: GradioFileData[];
change: GalleryOutputEntry[];
upload: undefined;
clear: undefined;
}>();
$: _value = normalise_file(value, root, root_url);
if (value) {
_value = null
if (imgElem)
imgElem.src = convertComfyOutputToComfyURL(value[0])
}
$: if (!(_value && _value.length > 0 && imgElem)) {
imgWidth = 1
@@ -41,7 +45,6 @@
}
function onChange() {
value = _value || []
dispatch("change", value)
}
@@ -55,7 +58,7 @@
interface GradioUploadResponse {
error?: string;
files?: Array<string>;
files?: Array<GalleryOutputEntry>;
}
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
@@ -85,7 +88,12 @@
}
else {
// bare filename of image
files.push((r as ComfyUploadImageAPIResponse).name)
const resp = r as ComfyUploadImageAPIResponse;
files.push({
filename: resp.name,
subfolder: "",
type: "input"
})
}
}
@@ -133,21 +141,12 @@
}
pending_upload = false;
_value.forEach(
(file_data: GradioFileData, i: number) => {
if (response.files) {
file_data.orig_name = file_data.name;
file_data.name = response.files[i];
file_data.is_file = true;
}
}
);
if (response.error) {
notify(response.error, { type: "error" })
}
value = normalise_file(_value, root, root_url) as GradioFileData[];
value = response.files;
onChange();
onUpload();
});
@@ -164,14 +163,13 @@
function handle_clear(_e: CustomEvent<null>) {
_value = null;
value = [];
onChange();
onClear();
}
function getImageUrl(image: GradioFileData) {
const baseUrl = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams({ filename: image.name, subfolder: "", type: "input" })
return `${baseUrl}/view?${params}`
function convertGradioUpload(e: CustomEvent<GradioFileData[]>) {
_value = e.detail
}
</script>
@@ -194,7 +192,7 @@
{#if _value && _value.length > 0 && !pending_upload}
{@const firstImage = _value[0]}
<ModifyUpload on:clear={handle_clear} absolute />
<img src={getImageUrl(firstImage)}
<img src={converGradioFileDataToComfyURL(firstImage)}
alt={firstImage.orig_name}
bind:this={imgElem}
bind:naturalWidth={imgWidth}
@@ -204,13 +202,13 @@
<Upload
file_count={fileCount}
filetype="image/*"
on:change={({ detail }) => (value = detail)}
on:change={convertGradioUpload}
on:load={handle_upload}
bind:dragging
on:clear
on:select
parse_to_data_url={false}
>
>
<UploadText type="file" />
</Upload>
{/if}

View File

@@ -890,7 +890,7 @@ export interface ComfyImageUploadProperties extends ComfyWidgetProperties {
fileCount: "single" | "multiple" // gradio File component format
}
export class ComfyImageUploadNode extends ComfyWidgetNode<Array<GradioFileData>> {
export class ComfyImageUploadNode extends ComfyWidgetNode<GalleryOutputEntry[]> {
override properties: ComfyImageUploadProperties = {
defaultValue: [],
tags: [],
@@ -923,24 +923,38 @@ export class ComfyImageUploadNode extends ComfyWidgetNode<Array<GradioFileData>>
super(name, [])
}
override parseValue(value: any): GradioFileData[] {
override parseValue(value: any): GalleryOutputEntry[] {
if (value == null)
return []
if (typeof value === "string" && value !== "") { // Single filename
return [{ name: value, data: value, orig_name: value, is_file: true }]
const isComfyImageSpec = (value: any): boolean => {
return value && typeof value === "object" && "filename" in value && "type" in value
}
else {
return []
if (typeof value === "string") {
// Single filename
return [{ filename: value, subfolder: "", type: "input" }]
}
else if (isComfyImageSpec(value)) {
// Single ComfyUI file
return [value]
}
else if (Array.isArray(value)) {
if (value.every(v => typeof v === "string"))
return value.map(filename => { return { filename, subfolder: "", type: "input" } })
else if (value.every(isComfyImageSpec))
return value
}
return []
}
override onExecute(param: any, options: object) {
super.onExecute(param, options);
const value = get(this.value)
if (value.length > 0 && value[0].name) {
this.setOutputData(0, value[0].name) // TODO when ComfyUI LoadImage supports loading an image batch
if (value.length > 0) {
this.setOutputData(0, value[0].filename) // TODO when ComfyUI LoadImage supports loading an image batch
this.setOutputData(1, this.imageSize[0])
this.setOutputData(2, this.imageSize[1])
this.setOutputData(3, value.length)
@@ -953,7 +967,7 @@ export class ComfyImageUploadNode extends ComfyWidgetNode<Array<GradioFileData>>
}
}
override formatValue(value: GradioFileData[]): string {
override formatValue(value: GalleryOutputEntry[]): string {
return `Images: ${value?.length || 0}`
}
}

View File

@@ -6,7 +6,7 @@ import { get } from "svelte/store"
import layoutState from "$lib/stores/layoutState"
import type { SvelteComponentDev } from "svelte/internal";
import type { SerializedLGraph } from "@litegraph-ts/core";
import type { GalleryOutput, GalleryOutputEntry } from "./nodes/ComfyWidgetNodes";
import type { FileNameOrGalleryData, GalleryOutput, GalleryOutputEntry } from "./nodes/ComfyWidgetNodes";
import type { FileData as GradioFileData } from "@gradio/upload";
export function clamp(n: number, min: number, max: number): number {
@@ -139,12 +139,32 @@ export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileDat
});
}
export function convertComfyOutputToComfyURL(output: GalleryOutputEntry): string {
export function convertComfyOutputToComfyURL(output: FileNameOrGalleryData): string {
if (typeof output === "string")
return output;
const params = new URLSearchParams(output)
const url = `http://${location.hostname}:8188` // TODO make configurable
return url + "/view?" + params
}
export function converGradioFileDataToComfyURL(image: GradioFileData, type: "input" | "output" | "temp" = "input"): string {
const baseUrl = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams({ filename: image.name, subfolder: "", type })
return `${baseUrl}/view?${params}`
}
export function convertGradioFileDataToComfyOutput(fileData: GradioFileData, type: "input" | "output" | "temp" = "input"): GalleryOutputEntry {
if (!fileData.is_file)
throw "Can't convert blob data to comfy output!"
return {
filename: fileData.name,
subfolder: "",
type
}
}
export function convertFilenameToComfyURL(filename: string,
subfolder: string = "",
type: "input" | "output" | "temp" = "output"): string {

View File

@@ -4,10 +4,14 @@
import { get, type Writable, writable } from "svelte/store";
import Modal from "$lib/components/Modal.svelte";
import { Button } from "@gradio/button";
import type { ComfyImageEditorNode, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
import type { ComfyImageEditorNode, GalleryOutputEntry, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
import { Embed as Klecks, KL, KlApp, klHistory, type KlAppOptionsEmbed } from "klecks";
import type { FileData as GradioFileData } from "@gradio/upload";
import "klecks/style/style.scss";
import ImageUpload from "$lib/components/ImageUpload.svelte";
import { uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
import notify from "$lib/notify";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
@@ -17,6 +21,10 @@
let leftUrl: string = ""
let rightUrl: string = ""
let imgElem: HTMLImageElement | null = null
let imgWidth: number = 0;
let imgHeight: number = 0;
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
@@ -65,8 +73,32 @@
showModal = false;
}
function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
const data = kl.getPNG();
const FILENAME: string = "ComfyUITemp.png";
const SUBFOLDER: string = "ComfyBox_Editor";
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
const blob = kl.getPNG();
const formData = new FormData();
formData.append("image", blob, FILENAME);
const entry: GalleryOutputEntry = {
filename: FILENAME,
subfolder: SUBFOLDER,
type: "input"
}
await uploadImageToComfyUI(entry)
.then((resp: ComfyUploadImageAPIResponse) => {
entry.filename = resp.name;
$nodeValue = [entry]
onSuccess();
})
.catch(err => {
notify(`Failed to upload image from editor: ${err}`, { type: "error", timeout: 10000 })
$nodeValue = []
onError();
})
}
function generateBlankImage(fill: string = "#fff"): HTMLCanvasElement {
@@ -110,6 +142,10 @@
kl.klApp?.out("yo");
}, 1000);
}
function onUploadChanged(e: CustomEvent<GradioFileData[]>) {
}
</script>
<div class="wrapper comfy-image-editor">
@@ -123,6 +159,17 @@
<div class="image-editor-root" bind:this={editorRoot} />
</Modal>
<div class="comfy-image-editor-panel">
<ImageUpload value={$nodeValue}
{isMobile}
bind:imgWidth
bind:imgHeight
bind:imgElem
fileCount={"single"}
elem_classes={[]}
style={""}
label={"Image"}
on:change={onUploadChanged}
/>
<Block>
<BlockTitle>Image editor.</BlockTitle>
<Button variant="secondary" on:click={openImageEditor}>

View File

@@ -1,37 +1,27 @@
<script lang="ts">
import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { File as FileIcon } from "@gradio/icons";
import ImageUpload from "$lib/components/ImageUpload.svelte"
import type { WidgetLayout } from "$lib/stores/layoutState";
import type { Writable } from "svelte/store";
import type { ComfyGalleryNode, ComfyImageUploadNode, GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes";
import type { FileData as GradioFileData } from "@gradio/upload";
import UploadText from "$lib/components/gradio/app/UploadText.svelte";
import { tick } from "svelte";
import notify from "$lib/notify";
import type { Vector2 } from "@litegraph-ts/core";
import type { ComfyGalleryNode, ComfyImageUploadNode, GalleryOutputEntry, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyImageUploadNode | null = null;
let nodeValue: Writable<Array<GradioFileData>> | null = null;
let nodeValue: Writable<GalleryOutputEntry[]> | null = null;
let propsChanged: Writable<number> | null = null;
let dragging = false;
let pending_upload = false;
let old_value: Array<GradioFileData> | null = null;
let imgWidth: number = 1;
let imgHeight: number = 1;
$: widget && setNodeValue(widget);
$: if (!(node && $nodeValue && $nodeValue.length > 0)) {
node.imageSize = [1, 1]
node.imageSize = [0, 0]
}
else if (imgWidth > 1 || imgHeight > 1) {
else if (imgWidth > 0 && imgHeight > 0) {
node.imageSize = [imgWidth, imgHeight]
}
else {
node.imageSize = [1, 1]
node.imageSize = [0, 0]
}
function setNodeValue(widget: WidgetLayout) {
@@ -42,22 +32,28 @@
}
};
function onChange(e: CustomEvent<GradioFileData[]>) {
function onChange(e: CustomEvent<GalleryOutputEntry[]>) {
console.warn("ONCHANGE!!!", e.detail)
$nodeValue = e.detail || []
}
function onClear(e: CustomEvent<GalleryOutputEntry[]>) {
console.warn("ONCLEAR!!!", e.detail)
$nodeValue = []
}
</script>
<div class="wrapper gradio-file comfy-image-upload" style={widget.attrs.style}>
{#if widget && node && nodeValue}
<ImageUpload value={$nodeValue}
{isMobile}
bind:imgWidth
bind:imgHeight
bind:fileCount={node.properties.fileCount}
elem_classes={widget.attrs.classes.split(",")}
style={widget.attrs.style}
label={widget.attrs.title}
on:change={onChange}/>
on:change={onChange}
on:clear={onClear}/>
{/if}
</div>