@@ -61,7 +61,7 @@
|
||||
],
|
||||
"title": "UI.Gallery",
|
||||
"properties": {
|
||||
"tags": [],
|
||||
"tags": ["gen"],
|
||||
"defaultValue": [],
|
||||
"index": 3,
|
||||
"updateMode": "append",
|
||||
@@ -1694,7 +1694,7 @@
|
||||
],
|
||||
"title": "UI.Gallery",
|
||||
"properties": {
|
||||
"tags": [],
|
||||
"tags": ["hr"],
|
||||
"defaultValue": [],
|
||||
"index": 1,
|
||||
"updateMode": "append",
|
||||
|
||||
@@ -101,6 +101,7 @@ export type ComfyUIPromptExtraData = {
|
||||
}
|
||||
|
||||
type ComfyAPIEvents = {
|
||||
// JSON
|
||||
status: (status: ComfyAPIStatusResponse | null, error?: Error | null) => void,
|
||||
progress: (progress: Progress) => void,
|
||||
reconnecting: () => void,
|
||||
@@ -111,6 +112,9 @@ type ComfyAPIEvents = {
|
||||
execution_cached: (promptID: PromptID, nodes: ComfyNodeID[]) => void,
|
||||
execution_interrupted: (error: ComfyInterruptedError) => void,
|
||||
execution_error: (error: ComfyExecutionError) => void,
|
||||
|
||||
// Binary
|
||||
b_preview: (imageBlob: Blob) => void
|
||||
}
|
||||
|
||||
export default class ComfyAPI {
|
||||
@@ -176,6 +180,7 @@ export default class ComfyAPI {
|
||||
this.socket = new WebSocket(
|
||||
`ws${window.location.protocol === "https:" ? "s" : ""}://${hostname}:${port}/ws${existingSession}`
|
||||
);
|
||||
this.socket.binaryType = "arraybuffer";
|
||||
|
||||
this.socket.addEventListener("open", () => {
|
||||
opened = true;
|
||||
@@ -204,6 +209,31 @@ export default class ComfyAPI {
|
||||
|
||||
this.socket.addEventListener("message", (event) => {
|
||||
try {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
const view = new DataView(event.data);
|
||||
const eventType = view.getUint32(0);
|
||||
const buffer = event.data.slice(4);
|
||||
switch (eventType) {
|
||||
case 1:
|
||||
const view2 = new DataView(event.data);
|
||||
const imageType = view2.getUint32(0)
|
||||
let imageMime: string
|
||||
switch (imageType) {
|
||||
case 1:
|
||||
default:
|
||||
imageMime = "image/jpeg";
|
||||
break;
|
||||
case 2:
|
||||
imageMime = "image/png"
|
||||
}
|
||||
const imageBlob = new Blob([buffer.slice(4)], { type: imageMime });
|
||||
this.eventBus.emit("b_preview", imageBlob);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown binary websocket message of type ${eventType}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const msg = JSON.parse(event.data);
|
||||
switch (msg.type) {
|
||||
case "status":
|
||||
@@ -237,6 +267,7 @@ export default class ComfyAPI {
|
||||
default:
|
||||
console.warn("Unhandled message:", event.data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling message", event.data, error);
|
||||
}
|
||||
|
||||
@@ -651,6 +651,10 @@ export default class ComfyApp {
|
||||
}
|
||||
});
|
||||
|
||||
this.api.addEventListener("b_preview", (imageBlob: Blob) => {
|
||||
queueState.previewUpdated(imageBlob);
|
||||
});
|
||||
|
||||
const config = get(configState);
|
||||
|
||||
if (config.pollSystemStatsInterval > 0) {
|
||||
|
||||
@@ -386,6 +386,9 @@
|
||||
<span style="display: inline-flex !important; padding: 0 0.75rem;">
|
||||
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
|
||||
</span>
|
||||
<span style="display: inline-flex !important; padding: 0 0.75rem;">
|
||||
<Checkbox label="Hide Previews" bind:value={$uiState.hidePreviews}/>
|
||||
</span>
|
||||
<!-- <span class="label" for="ui-edit-mode">
|
||||
<BlockTitle>UI Edit mode</BlockTitle>
|
||||
<select id="ui-edit-mode" name="ui-edit-mode" bind:value={$uiState.uiEditMode}>
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
<script lang="ts" context="module">
|
||||
export type QueueUIEntryStatus = QueueEntryStatus | "pending" | "running";
|
||||
|
||||
export type QueueUIEntry = {
|
||||
entry: QueueEntry,
|
||||
message: string,
|
||||
submessage: string,
|
||||
date?: string,
|
||||
status: QueueUIEntryStatus,
|
||||
images?: string[], // URLs
|
||||
details?: string, // shown in a tooltip on hover
|
||||
error?: WorkflowError
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState";
|
||||
import ProgressBar from "./ProgressBar.svelte";
|
||||
@@ -20,7 +5,7 @@
|
||||
import Spinner from "./Spinner.svelte";
|
||||
import PromptDisplay from "./PromptDisplay.svelte";
|
||||
import { List, ListUl, Grid } from "svelte-bootstrap-icons";
|
||||
import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo, truncateString } from "$lib/utils"
|
||||
import { getNodeInfo, type ComfyImageLocation } from "$lib/utils"
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { QueueItemType } from "$lib/api";
|
||||
import { Button } from "@gradio/button";
|
||||
@@ -31,7 +16,7 @@
|
||||
import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte";
|
||||
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
|
||||
import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte";
|
||||
import uiQueueState from "$lib/stores/uiQueueState";
|
||||
import uiQueueState, { type QueueUIEntry } from "$lib/stores/uiQueueState";
|
||||
|
||||
export let app: ComfyApp;
|
||||
|
||||
@@ -125,7 +110,7 @@
|
||||
let showModal = false;
|
||||
let expandAll = false;
|
||||
let selectedPrompt = null;
|
||||
let selectedImages = [];
|
||||
let selectedImages: ComfyImageLocation[] = [];
|
||||
function showPrompt(entry: QueueUIEntry) {
|
||||
if (entry.error != null) {
|
||||
showModal = false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { QueueItemType } from "$lib/api";
|
||||
import { showLightbox } from "$lib/utils";
|
||||
import { convertComfyOutputToComfyURL, showLightbox } from "$lib/utils";
|
||||
import type { QueueUIEntry } from "./ComfyQueue.svelte";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
allEntries = []
|
||||
for (const entry of entries) {
|
||||
for (const image of entry.images) {
|
||||
allEntries.push([entry, image]);
|
||||
allEntries.push([entry, convertComfyOutputToComfyURL(image, true)]);
|
||||
}
|
||||
}
|
||||
allImages = allEntries.map(p => p[1]);
|
||||
@@ -56,6 +56,7 @@
|
||||
<img class="grid-entry-image"
|
||||
on:click={(e) => handleClick(e, entry, i)}
|
||||
src={image}
|
||||
loading="lazy"
|
||||
alt="thumbnail" />
|
||||
</div>
|
||||
{/each}
|
||||
@@ -130,6 +131,8 @@
|
||||
.grid-entry-image {
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { QueueItemType } from "$lib/api";
|
||||
import { showLightbox, truncateString } from "$lib/utils";
|
||||
import type { QueueUIEntry } from "./ComfyQueue.svelte";
|
||||
import { convertComfyOutputToComfyURL, showLightbox, truncateString } from "$lib/utils";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import type { QueueUIEntry } from "$lib/stores/uiQueueState";
|
||||
|
||||
export let entries: QueueUIEntry[] = [];
|
||||
export let showPrompt: (entry: QueueUIEntry) => void;
|
||||
@@ -39,11 +39,13 @@
|
||||
<div class="list-entry-images"
|
||||
style="--cols: {Math.ceil(Math.sqrt(Math.min(entry.images.length, 4)))}" >
|
||||
{#each entry.images.slice(0, 4) as image, i}
|
||||
{@const imageURL = convertComfyOutputToComfyURL(image, true)}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<img class="list-entry-image"
|
||||
on:click={(e) => showLightbox(entry.images, i, e)}
|
||||
src={image}
|
||||
src={imageURL}
|
||||
loading="lazy"
|
||||
alt="thumbnail" />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Gallery from "$lib/components/gradio/gallery/Gallery.svelte";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import type { Styles } from "@gradio/utils";
|
||||
import { comfyFileToComfyBoxMetadata, comfyURLToComfyFile, countNewLines } from "$lib/utils";
|
||||
import { comfyFileToComfyBoxMetadata, comfyURLToComfyFile, countNewLines, type ComfyImageLocation, convertComfyOutputToComfyURL } from "$lib/utils";
|
||||
import ReceiveOutputTargets from "./modal/ReceiveOutputTargets.svelte";
|
||||
import workflowState, { type ComfyBoxWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
|
||||
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
|
||||
@@ -17,7 +17,7 @@
|
||||
const splitLength = 50;
|
||||
|
||||
export let prompt: SerializedPromptInputsAll;
|
||||
export let images: string[] = []; // list of image URLs to ComfyUI's /view? endpoint
|
||||
export let images: ComfyImageLocation[] = [];
|
||||
export let isMobile: boolean = false;
|
||||
export let expandAll: boolean = false;
|
||||
export let closeModal: () => void;
|
||||
@@ -36,10 +36,7 @@
|
||||
let litegraphType = "(none)"
|
||||
|
||||
$: if (images.length > 0) {
|
||||
// since the image links come from gradio, have to parse the URL for the
|
||||
// ComfyImageLocation params
|
||||
comfyBoxImages = images.map(comfyURLToComfyFile)
|
||||
.map(comfyFileToComfyBoxMetadata);
|
||||
comfyBoxImages = images.map(comfyFileToComfyBoxMetadata);
|
||||
}
|
||||
else {
|
||||
comfyBoxImages = []
|
||||
@@ -199,7 +196,7 @@
|
||||
<div class="image-container">
|
||||
<Block>
|
||||
<Gallery
|
||||
value={images}
|
||||
value={images.map(convertComfyOutputToComfyURL)}
|
||||
label=""
|
||||
show_label={false}
|
||||
style={galleryStyle}
|
||||
|
||||
@@ -9,7 +9,8 @@ import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
||||
index: number | null,
|
||||
updateMode: "replace" | "append",
|
||||
autoSelectOnUpdate: boolean
|
||||
autoSelectOnUpdate: boolean,
|
||||
showPreviews: boolean
|
||||
}
|
||||
|
||||
export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
||||
@@ -18,7 +19,8 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
||||
defaultValue: [],
|
||||
index: 0,
|
||||
updateMode: "replace",
|
||||
autoSelectOnUpdate: true
|
||||
autoSelectOnUpdate: true,
|
||||
showPreviews: true
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
|
||||
@@ -119,6 +119,36 @@ const defNotifications: ConfigDefEnum<"notifications", NotificationState> = {
|
||||
}
|
||||
};
|
||||
|
||||
export enum OutputThumbnailsMode {
|
||||
Auto,
|
||||
AlwaysThumbnail,
|
||||
AlwaysFullSize
|
||||
}
|
||||
|
||||
const defOutputThumbnails: ConfigDefEnum<"outputThumbnails", OutputThumbnailsMode> = {
|
||||
name: "outputThumbnails",
|
||||
type: "enum",
|
||||
defaultValue: OutputThumbnailsMode.Auto,
|
||||
category: "ui",
|
||||
description: "If enabled, send back smaller sized output image thumbnails for gallery/queue/history. Enable if you have slow network or are using Colab.",
|
||||
options: {
|
||||
values: [
|
||||
{
|
||||
value: OutputThumbnailsMode.Auto,
|
||||
label: "Autodetect"
|
||||
},
|
||||
{
|
||||
value: OutputThumbnailsMode.AlwaysThumbnail,
|
||||
label: "Always use thumbnails"
|
||||
},
|
||||
{
|
||||
value: OutputThumbnailsMode.AlwaysFullSize,
|
||||
label: "Always use full size"
|
||||
},
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const defAlwaysStripUserState: ConfigDefBoolean<"alwaysStripUserState"> = {
|
||||
name: "alwaysStripUserState",
|
||||
type: "boolean",
|
||||
@@ -207,6 +237,7 @@ export const CONFIG_DEFS = [
|
||||
defComfyUIHostname,
|
||||
defComfyUIPort,
|
||||
defNotifications,
|
||||
defOutputThumbnails,
|
||||
defAlwaysStripUserState,
|
||||
defPromptForWorkflowName,
|
||||
defConfirmWhenUnloadingUnsavedChanges,
|
||||
|
||||
@@ -615,6 +615,14 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
validNodeTypes: ["ui/gallery"],
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
name: "showPreviews",
|
||||
type: "boolean",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
validNodeTypes: ["ui/gallery"],
|
||||
defaultValue: true
|
||||
},
|
||||
|
||||
// ImageUpload
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ type QueueStateOps = {
|
||||
executionCached: (promptID: PromptID, nodes: ComfyNodeID[]) => void,
|
||||
executionError: (error: ComfyExecutionError) => CompletedQueueEntry | null,
|
||||
progressUpdated: (progress: Progress) => void
|
||||
previewUpdated: (imageBlob: Blob) => void
|
||||
getQueueEntry: (promptID: PromptID) => QueueEntry | null;
|
||||
afterQueued: (workflowID: WorkflowInstID, promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
|
||||
queueItemDeleted: (type: QueueItemType, id: PromptID) => void;
|
||||
@@ -88,6 +89,11 @@ export type QueueState = {
|
||||
*/
|
||||
runningNodeID: ComfyNodeID | null;
|
||||
|
||||
/*
|
||||
* Currently executing prompt if any
|
||||
*/
|
||||
runningPromptID: PromptID | null;
|
||||
|
||||
/*
|
||||
* Nodes which should be rendered as "executing" in the frontend (green border).
|
||||
* This includes the running node and all its parent subgraphs
|
||||
@@ -98,6 +104,12 @@ export type QueueState = {
|
||||
* Progress for the current node reported by the frontend
|
||||
*/
|
||||
progress: Progress | null,
|
||||
|
||||
/*
|
||||
* Image preview URL
|
||||
*/
|
||||
previewURL: string | 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
|
||||
@@ -115,6 +127,7 @@ const store: Writable<QueueState> = writable({
|
||||
runningNodeID: null,
|
||||
executingNodes: new Set(),
|
||||
progress: null,
|
||||
preview: null,
|
||||
isInterrupting: false
|
||||
})
|
||||
|
||||
@@ -171,6 +184,19 @@ function progressUpdated(progress: Progress) {
|
||||
})
|
||||
}
|
||||
|
||||
function previewUpdated(imageBlob: Blob) {
|
||||
console.debug("[queueState] previewUpdated", imageBlob?.type)
|
||||
store.update(s => {
|
||||
if (s.runningNodeID == null) {
|
||||
s.previewURL = null;
|
||||
return s;
|
||||
}
|
||||
|
||||
s.previewURL = URL.createObjectURL(imageBlob);
|
||||
return s;
|
||||
})
|
||||
}
|
||||
|
||||
function statusUpdated(status: ComfyAPIStatusResponse | null) {
|
||||
console.debug("[queueState] statusUpdated", status)
|
||||
store.update((s) => {
|
||||
@@ -296,6 +322,7 @@ function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null)
|
||||
entry.nodesRan.add(runningNodeID)
|
||||
}
|
||||
s.runningNodeID = runningNodeID;
|
||||
s.runningPromptID = promptID;
|
||||
|
||||
if (entry?.extraData?.workflowID) {
|
||||
const workflow = workflowState.getWorkflow(entry.extraData.workflowID);
|
||||
@@ -337,7 +364,9 @@ function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null)
|
||||
console.debug("[queueState] Could not find in pending! (executingUpdated)", promptID)
|
||||
}
|
||||
s.progress = null;
|
||||
s.previewURL = null;
|
||||
s.runningNodeID = null;
|
||||
s.runningPromptID = null;
|
||||
s.executingNodes.clear();
|
||||
}
|
||||
entry_ = entry;
|
||||
@@ -362,7 +391,9 @@ function executionCached(promptID: PromptID, nodes: ComfyNodeID[]) {
|
||||
}
|
||||
s.isInterrupting = false; // TODO move to start
|
||||
s.progress = null;
|
||||
s.previewURL = null;
|
||||
s.runningNodeID = null;
|
||||
s.runningPromptID = null;
|
||||
s.executingNodes.clear();
|
||||
return s
|
||||
})
|
||||
@@ -380,7 +411,9 @@ function executionError(error: ComfyExecutionError): CompletedQueueEntry | null
|
||||
console.error("[queueState] Could not find in pending! (executionError)", error.prompt_id)
|
||||
}
|
||||
s.progress = null;
|
||||
s.previewURL = null;
|
||||
s.runningNodeID = null;
|
||||
s.runningPromptID = null;
|
||||
s.executingNodes.clear();
|
||||
return s
|
||||
})
|
||||
@@ -416,6 +449,7 @@ function executionStart(promptID: PromptID) {
|
||||
}
|
||||
s.isInterrupting = false;
|
||||
s.runningNodeID = null;
|
||||
s.runningPromptID = promptID;
|
||||
s.executingNodes.clear();
|
||||
return s
|
||||
})
|
||||
@@ -480,7 +514,9 @@ function queueCleared(type: QueueItemType) {
|
||||
s.queuePending.set([]);
|
||||
s.queueRemaining = 0;
|
||||
s.runningNodeID = null;
|
||||
s.runningPromptID = null;
|
||||
s.progress = null;
|
||||
s.previewURL = null;
|
||||
s.executingNodes.clear();
|
||||
}
|
||||
else {
|
||||
@@ -535,6 +571,7 @@ const queueStateStore: WritableQueueStateStore =
|
||||
historyUpdated,
|
||||
statusUpdated,
|
||||
progressUpdated,
|
||||
previewUpdated,
|
||||
executionStart,
|
||||
executingUpdated,
|
||||
executionCached,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { PromptID, QueueItemType } from '$lib/api';
|
||||
import type { ComfyImageLocation } from "$lib/utils";
|
||||
import { get, writable } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import queueState, { type CompletedQueueEntry, type QueueEntry } from './queueState';
|
||||
import queueState, { QueueEntryStatus, type CompletedQueueEntry, type QueueEntry } from './queueState';
|
||||
import type { WorkflowError } from './workflowState';
|
||||
import { convertComfyOutputToComfyURL } from '$lib/utils';
|
||||
|
||||
@@ -13,7 +14,7 @@ export type QueueUIEntry = {
|
||||
submessage: string,
|
||||
date?: string,
|
||||
status: QueueUIEntryStatus,
|
||||
images?: string[], // URLs
|
||||
images?: ComfyImageLocation[], // URLs
|
||||
details?: string, // shown in a tooltip on hover
|
||||
error?: WorkflowError
|
||||
}
|
||||
@@ -94,13 +95,12 @@ function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): Que
|
||||
|
||||
const thumbnails = entry.extraData?.thumbnails
|
||||
if (thumbnails) {
|
||||
result.images = thumbnails.map(convertComfyOutputToComfyURL);
|
||||
result.images = [...thumbnails]
|
||||
}
|
||||
|
||||
const outputs = Object.values(entry.outputs)
|
||||
.filter(o => o.images)
|
||||
.flatMap(o => o.images)
|
||||
.map(convertComfyOutputToComfyURL);
|
||||
if (outputs) {
|
||||
result.images = result.images.concat(outputs)
|
||||
}
|
||||
@@ -114,7 +114,6 @@ function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
|
||||
const images = Object.values(entry.entry.outputs)
|
||||
.filter(o => o.images)
|
||||
.flatMap(o => o.images)
|
||||
.map(convertComfyOutputToComfyURL);
|
||||
result.images = images
|
||||
|
||||
if (entry.message)
|
||||
@@ -132,6 +131,8 @@ function updateFromQueue(queuePending: QueueEntry[], queueRunning: QueueEntry[])
|
||||
// newest entries appear at the top
|
||||
s.queuedEntries = queuePending.map((e) => convertPendingEntry(e, "pending")).reverse();
|
||||
s.runningEntries = queueRunning.map((e) => convertPendingEntry(e, "running")).reverse();
|
||||
s.queuedEntries.sort((a, b) => a.entry.number - b.entry.number)
|
||||
s.runningEntries.sort((a, b) => a.entry.number - b.entry.number)
|
||||
s.queueUIEntries = s.queuedEntries.concat(s.runningEntries);
|
||||
console.warn("[ComfyQueue] BUILDQUEUE", s.queuedEntries.length, s.runningEntries.length)
|
||||
return s;
|
||||
|
||||
@@ -10,6 +10,7 @@ export type UIState = {
|
||||
autoAddUI: boolean,
|
||||
uiUnlocked: boolean,
|
||||
uiEditMode: UIEditMode,
|
||||
hidePreviews: boolean,
|
||||
|
||||
reconnecting: boolean,
|
||||
forceSaveUserState: boolean | null,
|
||||
@@ -30,6 +31,7 @@ const store: Writable<UIState> = writable(
|
||||
autoAddUI: true,
|
||||
uiUnlocked: false,
|
||||
uiEditMode: "widgets",
|
||||
hidePreviews: false,
|
||||
|
||||
reconnecting: false,
|
||||
forceSaveUserState: null,
|
||||
|
||||
139
src/lib/utils.ts
139
src/lib/utils.ts
@@ -9,6 +9,7 @@ import workflowState, { type WorkflowReceiveOutputTargets } from "./stores/workf
|
||||
import { ImageViewer } from "./ImageViewer";
|
||||
import configState from "$lib/stores/configState";
|
||||
import SendOutputModal, { type SendOutputModalResult } from "$lib/components/modal/SendOutputModal.svelte";
|
||||
import { OutputThumbnailsMode } from "./stores/configDefs";
|
||||
|
||||
export function clamp(n: number, min: number, max: number): number {
|
||||
if (max <= min)
|
||||
@@ -300,31 +301,80 @@ export function convertComfyOutputToGradio(output: SerializedPromptOutput): Grad
|
||||
|
||||
export function convertComfyOutputEntryToGradio(r: ComfyImageLocation): GradioFileData {
|
||||
const url = configState.getBackendURL();
|
||||
const params = new URLSearchParams(r)
|
||||
const fileData: GradioFileData = {
|
||||
name: r.filename,
|
||||
orig_name: r.filename,
|
||||
is_file: false,
|
||||
data: url + "/view?" + params
|
||||
data: convertComfyOutputToComfyURL(r)
|
||||
}
|
||||
return fileData
|
||||
}
|
||||
|
||||
export function convertComfyOutputToComfyURL(output: string | ComfyImageLocation): string {
|
||||
function convertComfyPreviewTypeToString(preview: ComfyImagePreviewType): string {
|
||||
const arr = []
|
||||
switch (preview.format) {
|
||||
case ComfyImagePreviewFormat.JPEG:
|
||||
arr.push("jpeg")
|
||||
break;
|
||||
case ComfyImagePreviewFormat.WebP:
|
||||
default:
|
||||
arr.push("webp")
|
||||
break;
|
||||
}
|
||||
|
||||
arr.push(String(preview.quality))
|
||||
|
||||
return arr.join(";")
|
||||
}
|
||||
|
||||
export function convertComfyOutputToComfyURL(output: string | ComfyImageLocation, thumbnail: boolean = false): string {
|
||||
if (typeof output === "string")
|
||||
return output;
|
||||
|
||||
const params = new URLSearchParams(output)
|
||||
const paramsObj = {
|
||||
filename: output.filename,
|
||||
subfolder: output.subfolder,
|
||||
type: output.type
|
||||
}
|
||||
|
||||
if (thumbnail) {
|
||||
let doThumbnail: boolean;
|
||||
|
||||
switch (get(configState).outputThumbnails) {
|
||||
case OutputThumbnailsMode.AlwaysFullSize:
|
||||
doThumbnail = false;
|
||||
break;
|
||||
case OutputThumbnailsMode.AlwaysThumbnail:
|
||||
doThumbnail = true;
|
||||
break;
|
||||
case OutputThumbnailsMode.Auto:
|
||||
default:
|
||||
// TODO detect colab, etc.
|
||||
if (isMobileBrowser(navigator.userAgent)) {
|
||||
doThumbnail = true;
|
||||
}
|
||||
else {
|
||||
doThumbnail = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (doThumbnail) {
|
||||
output.preview = {
|
||||
format: ComfyImagePreviewFormat.WebP,
|
||||
quality: 80
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (output.preview != null)
|
||||
paramsObj["preview"] = convertComfyPreviewTypeToString(output.preview)
|
||||
|
||||
const params = new URLSearchParams(paramsObj)
|
||||
const url = configState.getBackendURL();
|
||||
return url + "/view?" + params
|
||||
}
|
||||
|
||||
export function convertGradioFileDataToComfyURL(image: GradioFileData, type: ComfyUploadImageType = "input"): string {
|
||||
const baseUrl = configState.getBackendURL();
|
||||
const params = new URLSearchParams({ filename: image.name, subfolder: "", type })
|
||||
return `${baseUrl}/view?${params}`
|
||||
}
|
||||
|
||||
export function convertGradioFileDataToComfyOutput(fileData: GradioFileData, type: ComfyUploadImageType = "input"): ComfyImageLocation {
|
||||
if (!fileData.is_file)
|
||||
throw "Can't convert blob data to comfy output!"
|
||||
@@ -336,18 +386,6 @@ export function convertGradioFileDataToComfyOutput(fileData: GradioFileData, typ
|
||||
}
|
||||
}
|
||||
|
||||
export function convertFilenameToComfyURL(filename: string,
|
||||
subfolder: string = "",
|
||||
type: "input" | "output" | "temp" = "output"): string {
|
||||
const params = new URLSearchParams({
|
||||
filename,
|
||||
subfolder,
|
||||
type
|
||||
})
|
||||
const url = configState.getBackendURL();
|
||||
return url + "/view?" + params
|
||||
}
|
||||
|
||||
export function jsonToJsObject(json: string): string {
|
||||
// Try to parse, to see if it's real JSON
|
||||
JSON.parse(json);
|
||||
@@ -413,6 +451,16 @@ export interface SerializedPromptOutput {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export enum ComfyImagePreviewFormat {
|
||||
WebP = "webp",
|
||||
JPEG = "jpeg",
|
||||
}
|
||||
|
||||
export type ComfyImagePreviewType = {
|
||||
format: ComfyImagePreviewFormat,
|
||||
quality: number
|
||||
}
|
||||
|
||||
/** Raw output entry as received from ComfyUI's backend */
|
||||
export type ComfyImageLocation = {
|
||||
/* Filename with extension in the subfolder. */
|
||||
@@ -420,7 +468,19 @@ export type ComfyImageLocation = {
|
||||
/* Subfolder in the containing folder. */
|
||||
subfolder: string,
|
||||
/* Base ComfyUI folder where the image is located. */
|
||||
type: ComfyUploadImageType
|
||||
type: ComfyUploadImageType,
|
||||
/*
|
||||
* Preview information
|
||||
*
|
||||
* "format;quality"
|
||||
*
|
||||
* ex)
|
||||
* webp;50 -> webp, quality 50
|
||||
* webp;50 -> webp, quality 50
|
||||
* jpeg;80 -> rgb, jpeg, quality 80
|
||||
*
|
||||
*/
|
||||
preview?: ComfyImagePreviewType
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -544,27 +604,54 @@ export function comfyBoxImageToComfyURL(image: ComfyBoxImageMetadata): string {
|
||||
return convertComfyOutputToComfyURL(image.comfyUIFile)
|
||||
}
|
||||
|
||||
function parseComfyUIPreviewType(previewStr: string): ComfyImagePreviewType {
|
||||
let split = previewStr.split(";")
|
||||
let format = ComfyImagePreviewFormat.WebP;
|
||||
if (split[0] === "webp")
|
||||
format = ComfyImagePreviewFormat.WebP;
|
||||
else if (split[0] === "jpeg")
|
||||
format = ComfyImagePreviewFormat.JPEG;
|
||||
|
||||
let quality = parseInt(split[0])
|
||||
if (isNaN(quality))
|
||||
quality = 80
|
||||
|
||||
return { format, quality }
|
||||
}
|
||||
|
||||
export function comfyURLToComfyFile(urlString: string): ComfyImageLocation | null {
|
||||
const url = new URL(urlString);
|
||||
const params = new URLSearchParams(url.search);
|
||||
const filename = params.get("filename")
|
||||
const type = params.get("type") as ComfyUploadImageType;
|
||||
const subfolder = params.get("subfolder") || ""
|
||||
const previewStr = params.get("preview") || null;
|
||||
let preview = null
|
||||
|
||||
if (previewStr != null) {
|
||||
preview = parseComfyUIPreviewType(preview);
|
||||
}
|
||||
|
||||
// If at least filename and type exist then we're good
|
||||
if (filename != null && type != null) {
|
||||
return { filename, type, subfolder }
|
||||
return { filename, type, subfolder, preview }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function showLightbox(images: string[], index: number, e: Event) {
|
||||
export function showLightbox(images: ComfyImageLocation[] | string[], index: number, e: Event) {
|
||||
e.preventDefault()
|
||||
if (!images)
|
||||
return
|
||||
|
||||
ImageViewer.instance.showModal(images, index);
|
||||
let images_: string[]
|
||||
if (typeof images[0] === "object")
|
||||
images_ = (images as ComfyImageLocation[]).map(v => convertComfyOutputToComfyURL(v))
|
||||
else
|
||||
images_ = (images as string[])
|
||||
|
||||
ImageViewer.instance.showModal(images_, index);
|
||||
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
import { f7 } from "framework7-svelte";
|
||||
import type { ComfyGalleryNode } from "$lib/nodes/widgets";
|
||||
import { showMobileLightbox } from "$lib/components/utils";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { loadImage } from "./utils";
|
||||
import Spinner from "$lib/components/Spinner.svelte";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
@@ -25,6 +29,47 @@
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function tagsMatch(tags: string[] | null): boolean {
|
||||
if(tags != null && tags.length > 0)
|
||||
return node.properties.tags.length > 0 && node.properties.tags.every(t => tags.includes(t));
|
||||
else
|
||||
return node.properties.tags.length === 0;
|
||||
}
|
||||
|
||||
let previewURL: string | null;
|
||||
let previewImage: HTMLImageElement | null = null;
|
||||
let previewElem: HTMLImageElement | null = null
|
||||
$: {
|
||||
previewURL = $queueState.previewURL;
|
||||
|
||||
if (previewURL && $queueState.runningPromptID != null && !$uiState.hidePreviews && node.properties.showPreviews) {
|
||||
const queueEntry = queueState.getQueueEntry($queueState.runningPromptID)
|
||||
if (queueEntry != null) {
|
||||
const tags = queueEntry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs;
|
||||
if (tagsMatch(tags)) {
|
||||
loadImage(previewURL).then((img) => {
|
||||
previewImage = img;
|
||||
})
|
||||
}
|
||||
else {
|
||||
previewImage = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
previewImage = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
previewImage = null;
|
||||
}
|
||||
}
|
||||
|
||||
function showPreview() {
|
||||
}
|
||||
|
||||
function hidePreview() {
|
||||
}
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfyGalleryNode
|
||||
@@ -34,6 +79,8 @@
|
||||
imageHeight = node.imageHeight
|
||||
selected_image = node.selectedImage;
|
||||
forceSelectImage = node.forceSelectImage;
|
||||
previewURL = null;
|
||||
previewImage = null;
|
||||
|
||||
if ($nodeValue != null) {
|
||||
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
|
||||
@@ -108,6 +155,11 @@
|
||||
<div class="wrapper comfy-gallery-widget gradio-gallery" style={widget.attrs.style || ""}>
|
||||
<Block variant="solid" padding={false}>
|
||||
<div class="padding">
|
||||
{#if previewImage}
|
||||
<div class="comfy-gallery-preview" on:mouseover={hidePreview} on:mouseout={showPreview} >
|
||||
<img src={previewImage.src} bind:this={previewElem} on:mouseout={showPreview} />
|
||||
</div>
|
||||
{/if}
|
||||
<Gallery
|
||||
value={images}
|
||||
label={widget.attrs.title}
|
||||
@@ -153,6 +205,29 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .comfy-gallery-preview {
|
||||
opacity: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
.comfy-gallery-preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: var(--layer-top);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.1s linear;
|
||||
opacity: 100%;
|
||||
|
||||
> img {
|
||||
width: var(--size-full);
|
||||
height: var(--size-full);
|
||||
object-fit: contain;
|
||||
border: 5px dashed var(--secondary-400);
|
||||
}
|
||||
}
|
||||
|
||||
.padding {
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Page, Navbar, Block, Tabs, Tab, NavLeft, NavTitle, NavRight, Link, f7 } from "framework7-svelte"
|
||||
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
|
||||
import type ComfyApp from "$lib/components/ComfyApp";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import type { IDragItem, WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||
import workflowState, { type ComfyBoxWorkflow, type WorkflowInstID } from "$lib/stores/workflowState";
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
import { onMount } from "svelte";
|
||||
import GenToolbar from '../GenToolbar.svelte'
|
||||
import { partition, showLightbox } from "$lib/utils";
|
||||
import { convertComfyOutputToComfyURL, partition, showLightbox } from "$lib/utils";
|
||||
import uiQueueState, { type QueueUIEntry } from "$lib/stores/uiQueueState";
|
||||
import { showMobileLightbox } from "$lib/components/utils";
|
||||
import notify from "$lib/notify";
|
||||
@@ -33,7 +27,7 @@
|
||||
const _allEntries = []
|
||||
for (const entry of entries) {
|
||||
for (const image of entry.images) {
|
||||
_allEntries.push([entry, image]);
|
||||
_allEntries.push([entry, convertComfyOutputToComfyURL(image, true)]);
|
||||
}
|
||||
}
|
||||
allEntries = partition(_allEntries, gridCols);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
import { onMount } from "svelte";
|
||||
import GenToolbar from '../GenToolbar.svelte'
|
||||
import { partition, showLightbox, truncateString } from "$lib/utils";
|
||||
import { convertComfyOutputToComfyURL, partition, showLightbox, truncateString } from "$lib/utils";
|
||||
import uiQueueState, { type QueueUIEntry } from "$lib/stores/uiQueueState";
|
||||
import { showMobileLightbox } from "$lib/components/utils";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
function getCardImage(entry: QueueUIEntry): string {
|
||||
if (entry.images.length > 0)
|
||||
return entry.images[0]
|
||||
return convertComfyOutputToComfyURL(entry.images[0])
|
||||
return "https://cdn.framework7.io/placeholder/nature-1000x600-3.jpg"
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user