fix(nodes,ui): fix missed/canvas temp images in gallery (#5111)

## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [x] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission

## Description

Resolves two bugs introduced in #5106:

1. Linear UI images sometimes didn't make it to the gallery.

This was a race condition. The VAE decode nodes were handled by the
socketInvocationComplete listener. At that moment, the image was marked
as intermediate. Immediately after this node was handled, a
LinearUIOutputInvocation, introduced in #5106, was handled by
socketInvocationComplete. This node internally sets changed the image to
not intermediate.

During the handling of that socketInvocationComplete, RTK Query would
sometimes use its cache instead of retrieving the image DTO again. The
result is that the UI never got the message that the image was not
intermediate, so it wasn't added to the gallery.

This is resolved by refactoring the socketInvocationComplete listener.
We now skip the gallery processing for linear UI events, except for the
LinearUIOutputInvocation. Images now always make it to the gallery, and
network requests to get image DTOs are substantially reduced.

2. Canvas temp images always went into the gallery

The LinearUIOutputInvocation was always setting its image's
is_intermediate to false. This included all canvas images and resulted
in all canvas temp images going to gallery.

This is resolved by making LinearUIOutputInvocation set is_intermediate
based on `self.is_intermediate`. The behaviour now more or less
mirroring the behaviour of is_intermediate on other image-outputting
nodes, except it doesn't save the image again - only changes it.

One extra minor change - LinearUIOutputInvocation only changes
is_intermediate if it differs from the image's current setting. Very
minor optimisation.

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue
https://discord.com/channels/1020123559063990373/1149513625321603162/1174721072826945638

## QA Instructions, Screenshots, Recordings

Try to reproduce the issues described int he discord thread:
- Images should always go to the gallery from txt2img and img2img
- Canvas temp images should not go to the gallery unless auto-save is
enabled
<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->
This commit is contained in:
Millun Atluri 2023-11-17 08:05:43 +11:00 committed by GitHub
commit 47f3515745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 53 additions and 12 deletions

View File

@ -1024,7 +1024,7 @@ class SaveImageInvocation(BaseInvocation, WithWorkflow, WithMetadata):
title="Linear UI Image Output", title="Linear UI Image Output",
tags=["primitives", "image"], tags=["primitives", "image"],
category="primitives", category="primitives",
version="1.0.0", version="1.0.1",
use_cache=False, use_cache=False,
) )
class LinearUIOutputInvocation(BaseInvocation, WithWorkflow, WithMetadata): class LinearUIOutputInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@ -1039,7 +1039,10 @@ class LinearUIOutputInvocation(BaseInvocation, WithWorkflow, WithMetadata):
if self.board: if self.board:
context.services.board_images.add_image_to_board(self.board.board_id, self.image.image_name) context.services.board_images.add_image_to_board(self.board.board_id, self.image.image_name)
context.services.images.update(self.image.image_name, changes=ImageRecordChanges(is_intermediate=False)) if image_dto.is_intermediate != self.is_intermediate:
context.services.images.update(
self.image.image_name, changes=ImageRecordChanges(is_intermediate=self.is_intermediate)
)
return ImageOutput( return ImageOutput(
image=ImageField(image_name=self.image.image_name), image=ImageField(image_name=self.image.image_name),

View File

@ -7,7 +7,10 @@ import {
imageSelected, imageSelected,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import { IMAGE_CATEGORIES } from 'features/gallery/store/types'; import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
import { CANVAS_OUTPUT } from 'features/nodes/util/graphBuilders/constants'; import {
LINEAR_UI_OUTPUT,
nodeIDDenyList,
} from 'features/nodes/util/graphBuilders/constants';
import { boardsApi } from 'services/api/endpoints/boards'; import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
import { isImageOutput } from 'services/api/guards'; import { isImageOutput } from 'services/api/guards';
@ -19,7 +22,7 @@ import {
import { startAppListening } from '../..'; import { startAppListening } from '../..';
// These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them // These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them
const nodeDenylist = ['load_image', 'image']; const nodeTypeDenylist = ['load_image', 'image'];
export const addInvocationCompleteEventListener = () => { export const addInvocationCompleteEventListener = () => {
startAppListening({ startAppListening({
@ -32,22 +35,31 @@ export const addInvocationCompleteEventListener = () => {
`Invocation complete (${action.payload.data.node.type})` `Invocation complete (${action.payload.data.node.type})`
); );
const { result, node, queue_batch_id } = data; const { result, node, queue_batch_id, source_node_id } = data;
// This complete event has an associated image output // This complete event has an associated image output
if (isImageOutput(result) && !nodeDenylist.includes(node.type)) { if (
isImageOutput(result) &&
!nodeTypeDenylist.includes(node.type) &&
!nodeIDDenyList.includes(source_node_id)
) {
const { image_name } = result.image; const { image_name } = result.image;
const { canvas, gallery } = getState(); const { canvas, gallery } = getState();
// This populates the `getImageDTO` cache // This populates the `getImageDTO` cache
const imageDTO = await dispatch( const imageDTORequest = dispatch(
imagesApi.endpoints.getImageDTO.initiate(image_name) imagesApi.endpoints.getImageDTO.initiate(image_name, {
).unwrap(); forceRefetch: true,
})
);
const imageDTO = await imageDTORequest.unwrap();
imageDTORequest.unsubscribe();
// Add canvas images to the staging area // Add canvas images to the staging area
if ( if (
canvas.batchIds.includes(queue_batch_id) && canvas.batchIds.includes(queue_batch_id) &&
[CANVAS_OUTPUT].includes(data.source_node_id) [LINEAR_UI_OUTPUT].includes(data.source_node_id)
) { ) {
dispatch(addImageToStagingArea(imageDTO)); dispatch(addImageToStagingArea(imageDTO));
} }

View File

@ -6,7 +6,7 @@ import {
Graph, Graph,
LinearUIOutputInvocation, LinearUIOutputInvocation,
} from 'services/api/types'; } from 'services/api/types';
import { REALESRGAN as ESRGAN, LINEAR_UI_OUTPUT } from './constants'; import { ESRGAN, LINEAR_UI_OUTPUT } from './constants';
import { addCoreMetadataNode, upsertMetadata } from './metadata'; import { addCoreMetadataNode, upsertMetadata } from './metadata';
type Arg = { type Arg = {

View File

@ -67,7 +67,7 @@ export const BATCH_PROMPT = 'batch_prompt';
export const BATCH_STYLE_PROMPT = 'batch_style_prompt'; export const BATCH_STYLE_PROMPT = 'batch_style_prompt';
export const METADATA_COLLECT = 'metadata_collect'; export const METADATA_COLLECT = 'metadata_collect';
export const MERGE_METADATA = 'merge_metadata'; export const MERGE_METADATA = 'merge_metadata';
export const REALESRGAN = 'esrgan'; export const ESRGAN = 'esrgan';
export const DIVIDE = 'divide'; export const DIVIDE = 'divide';
export const SCALE = 'scale_image'; export const SCALE = 'scale_image';
export const SDXL_MODEL_LOADER = 'sdxl_model_loader'; export const SDXL_MODEL_LOADER = 'sdxl_model_loader';
@ -82,6 +82,32 @@ export const SDXL_REFINER_INPAINT_CREATE_MASK = 'refiner_inpaint_create_mask';
export const SEAMLESS = 'seamless'; export const SEAMLESS = 'seamless';
export const SDXL_REFINER_SEAMLESS = 'refiner_seamless'; export const SDXL_REFINER_SEAMLESS = 'refiner_seamless';
// these image-outputting nodes are from the linear UI and we should not handle the gallery logic on them
// instead, we wait for LINEAR_UI_OUTPUT node, and handle it like any other image-outputting node
export const nodeIDDenyList = [
CANVAS_OUTPUT,
LATENTS_TO_IMAGE,
LATENTS_TO_IMAGE_HRF_HR,
NSFW_CHECKER,
WATERMARKER,
ESRGAN,
ESRGAN_HRF,
RESIZE_HRF,
LATENTS_TO_IMAGE_HRF_LR,
IMG2IMG_RESIZE,
INPAINT_IMAGE,
SCALED_INPAINT_IMAGE,
INPAINT_IMAGE_RESIZE_UP,
INPAINT_IMAGE_RESIZE_DOWN,
INPAINT_INFILL,
INPAINT_INFILL_RESIZE_DOWN,
INPAINT_FINAL_IMAGE,
INPAINT_CREATE_MASK,
INPAINT_MASK,
PASTE_IMAGE,
SCALE,
];
// friendly graph ids // friendly graph ids
export const TEXT_TO_IMAGE_GRAPH = 'text_to_image_graph'; export const TEXT_TO_IMAGE_GRAPH = 'text_to_image_graph';
export const IMAGE_TO_IMAGE_GRAPH = 'image_to_image_graph'; export const IMAGE_TO_IMAGE_GRAPH = 'image_to_image_graph';