Skip to content

Commit 4599517

Browse files
feat: add private node for linear UI image outputting
Add a LinearUIOutputInvocation node to be the new terminal node for Linear UI graphs. This node is private and hidden from the Workflow Editor, as it is an implementation detail. The Linear UI was using the Save Image node for this purpose. It allowed every linear graph to end a single node type, which handled saving metadata and board. This substantially reduced the complexity of the linear graphs. This caused two related issues: - Images were saved to disk twice - Noticeable delay between when an image was decoded and showed up in the UI To resolve this, the new LinearUIOutputInvocation node will handle adding an image to a board if one is provided. Metadata is no longer provided in this unified node. Instead, the metadata graph helpers now need to know the node to add metadata to and provide it to the last node that actually outputs an image. This is a `l2i` node for txt2img & img2img graphs, and a different image-outputting node for canvas graphs. HRF poses another complication, in that it changes the terminal node. To handle this, a new metadata util is added called `setMetadataReceivingNode()`. HRF calls this to change the node that should receive the graph's metadata. This resolves the duplicate images issue and improves perf without otherwise changing the user experience.
1 parent cc747c0 commit 4599517

23 files changed

Lines changed: 324 additions & 213 deletions

invokeai/app/invocations/image.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from PIL import Image, ImageChops, ImageFilter, ImageOps
99

1010
from invokeai.app.invocations.primitives import BoardField, ColorField, ImageField, ImageOutput
11-
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
11+
from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin
1212
from invokeai.app.shared.fields import FieldDescriptions
1313
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
1414
from invokeai.backend.image_util.safety_checker import SafetyChecker
@@ -1017,3 +1017,32 @@ def invoke(self, context: InvocationContext) -> ImageOutput:
10171017
width=image_dto.width,
10181018
height=image_dto.height,
10191019
)
1020+
1021+
1022+
@invocation(
1023+
"linear_ui_output",
1024+
title="Linear UI Image Output",
1025+
tags=["primitives", "image"],
1026+
category="primitives",
1027+
version="1.0.0",
1028+
use_cache=False,
1029+
)
1030+
class LinearUIOutputInvocation(BaseInvocation, WithWorkflow, WithMetadata):
1031+
"""Handles Linear UI Image Outputting tasks."""
1032+
1033+
image: ImageField = InputField(description=FieldDescriptions.image)
1034+
board: Optional[BoardField] = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct)
1035+
1036+
def invoke(self, context: InvocationContext) -> ImageOutput:
1037+
image_dto = context.services.images.get_dto(self.image.image_name)
1038+
1039+
if self.board:
1040+
context.services.board_images.add_image_to_board(self.board.board_id, self.image.image_name)
1041+
1042+
context.services.images.update(self.image.image_name, changes=ImageRecordChanges(is_intermediate=False))
1043+
1044+
return ImageOutput(
1045+
image=ImageField(image_name=self.image.image_name),
1046+
width=image_dto.width,
1047+
height=image_dto.height,
1048+
)

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
selectControlAdapterById,
99
} from 'features/controlAdapters/store/controlAdaptersSlice';
1010
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
11-
import { SAVE_IMAGE } from 'features/nodes/util/graphBuilders/constants';
1211
import { addToast } from 'features/system/store/systemSlice';
1312
import { t } from 'i18next';
1413
import { imagesApi } from 'services/api/endpoints/images';
@@ -38,6 +37,7 @@ export const addControlNetImageProcessedListener = () => {
3837
// ControlNet one-off procressing graph is just the processor node, no edges.
3938
// Also we need to grab the image.
4039

40+
const nodeId = ca.processorNode.id;
4141
const enqueueBatchArg: BatchConfig = {
4242
prepend: true,
4343
batch: {
@@ -46,27 +46,10 @@ export const addControlNetImageProcessedListener = () => {
4646
[ca.processorNode.id]: {
4747
...ca.processorNode,
4848
is_intermediate: true,
49-
image: { image_name: ca.controlImage },
50-
},
51-
[SAVE_IMAGE]: {
52-
id: SAVE_IMAGE,
53-
type: 'save_image',
54-
is_intermediate: true,
5549
use_cache: false,
50+
image: { image_name: ca.controlImage },
5651
},
5752
},
58-
edges: [
59-
{
60-
source: {
61-
node_id: ca.processorNode.id,
62-
field: 'image',
63-
},
64-
destination: {
65-
node_id: SAVE_IMAGE,
66-
field: 'image',
67-
},
68-
},
69-
],
7053
},
7154
runs: 1,
7255
},
@@ -90,7 +73,7 @@ export const addControlNetImageProcessedListener = () => {
9073
socketInvocationComplete.match(action) &&
9174
action.payload.data.queue_batch_id ===
9275
enqueueResult.batch.batch_id &&
93-
action.payload.data.source_node_id === SAVE_IMAGE
76+
action.payload.data.source_node_id === nodeId
9477
);
9578

9679
// We still have to check the output type

invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ const ImageMetadataActions = (props: Props) => {
157157
return null;
158158
}
159159

160+
console.log(metadata);
161+
160162
return (
161163
<>
162164
{metadata.created_by && (

invokeai/frontend/web/src/features/nodes/util/graphBuilders/addHrfToGraph.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
RESIZE_HRF,
2424
VAE_LOADER,
2525
} from './constants';
26-
import { upsertMetadata } from './metadata';
26+
import { setMetadataReceivingNode, upsertMetadata } from './metadata';
2727

2828
// Copy certain connections from previous DENOISE_LATENTS to new DENOISE_LATENTS_HRF.
2929
function copyConnectionsToDenoiseLatentsHrf(graph: NonNullableGraph): void {
@@ -369,4 +369,5 @@ export const addHrfToGraph = (
369369
hrf_enabled: hrfEnabled,
370370
hrf_method: hrfMethod,
371371
});
372+
setMetadataReceivingNode(graph, LATENTS_TO_IMAGE_HRF_HR);
372373
};

invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSaveImageNode.ts renamed to invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLinearUIOutputNode.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { RootState } from 'app/store/store';
22
import { NonNullableGraph } from 'features/nodes/types/types';
33
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
4-
import { SaveImageInvocation } from 'services/api/types';
4+
import { LinearUIOutputInvocation } from 'services/api/types';
55
import {
66
CANVAS_OUTPUT,
77
LATENTS_TO_IMAGE,
88
LATENTS_TO_IMAGE_HRF_HR,
9+
LINEAR_UI_OUTPUT,
910
NSFW_CHECKER,
10-
SAVE_IMAGE,
1111
WATERMARKER,
1212
} from './constants';
1313

1414
/**
1515
* Set the `use_cache` field on the linear/canvas graph's final image output node to False.
1616
*/
17-
export const addSaveImageNode = (
17+
export const addLinearUIOutputNode = (
1818
state: RootState,
1919
graph: NonNullableGraph
2020
): void => {
@@ -23,18 +23,18 @@ export const addSaveImageNode = (
2323
activeTabName === 'unifiedCanvas' ? !state.canvas.shouldAutoSave : false;
2424
const { autoAddBoardId } = state.gallery;
2525

26-
const saveImageNode: SaveImageInvocation = {
27-
id: SAVE_IMAGE,
28-
type: 'save_image',
26+
const linearUIOutputNode: LinearUIOutputInvocation = {
27+
id: LINEAR_UI_OUTPUT,
28+
type: 'linear_ui_output',
2929
is_intermediate,
3030
use_cache: false,
3131
board: autoAddBoardId === 'none' ? undefined : { board_id: autoAddBoardId },
3232
};
3333

34-
graph.nodes[SAVE_IMAGE] = saveImageNode;
34+
graph.nodes[LINEAR_UI_OUTPUT] = linearUIOutputNode;
3535

3636
const destination = {
37-
node_id: SAVE_IMAGE,
37+
node_id: LINEAR_UI_OUTPUT,
3838
field: 'image',
3939
};
4040

invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildAdHocUpscaleGraph.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { ESRGANModelName } from 'features/parameters/store/postprocessingSlice';
44
import {
55
ESRGANInvocation,
66
Graph,
7-
SaveImageInvocation,
7+
LinearUIOutputInvocation,
88
} from 'services/api/types';
9-
import { REALESRGAN as ESRGAN, SAVE_IMAGE } from './constants';
9+
import { REALESRGAN as ESRGAN, LINEAR_UI_OUTPUT } from './constants';
1010
import { addCoreMetadataNode, upsertMetadata } from './metadata';
1111

1212
type Arg = {
@@ -28,9 +28,9 @@ export const buildAdHocUpscaleGraph = ({
2828
is_intermediate: true,
2929
};
3030

31-
const saveImageNode: SaveImageInvocation = {
32-
id: SAVE_IMAGE,
33-
type: 'save_image',
31+
const linearUIOutputNode: LinearUIOutputInvocation = {
32+
id: LINEAR_UI_OUTPUT,
33+
type: 'linear_ui_output',
3434
use_cache: false,
3535
is_intermediate: false,
3636
board: autoAddBoardId === 'none' ? undefined : { board_id: autoAddBoardId },
@@ -40,7 +40,7 @@ export const buildAdHocUpscaleGraph = ({
4040
id: `adhoc-esrgan-graph`,
4141
nodes: {
4242
[ESRGAN]: realesrganNode,
43-
[SAVE_IMAGE]: saveImageNode,
43+
[LINEAR_UI_OUTPUT]: linearUIOutputNode,
4444
},
4545
edges: [
4646
{
@@ -49,14 +49,14 @@ export const buildAdHocUpscaleGraph = ({
4949
field: 'image',
5050
},
5151
destination: {
52-
node_id: SAVE_IMAGE,
52+
node_id: LINEAR_UI_OUTPUT,
5353
field: 'image',
5454
},
5555
},
5656
],
5757
};
5858

59-
addCoreMetadataNode(graph, {});
59+
addCoreMetadataNode(graph, {}, ESRGAN);
6060
upsertMetadata(graph, {
6161
esrgan_model: esrganModelName,
6262
});

invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
66
import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph';
77
import { addLoRAsToGraph } from './addLoRAsToGraph';
88
import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph';
9-
import { addSaveImageNode } from './addSaveImageNode';
9+
import { addLinearUIOutputNode } from './addLinearUIOutputNode';
1010
import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph';
1111
import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph';
1212
import { addVAEToGraph } from './addVAEToGraph';
@@ -308,24 +308,30 @@ export const buildCanvasImageToImageGraph = (
308308
});
309309
}
310310

311-
addCoreMetadataNode(graph, {
312-
generation_mode: 'img2img',
313-
cfg_scale,
314-
width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width,
315-
height: !isUsingScaledDimensions
316-
? height
317-
: scaledBoundingBoxDimensions.height,
318-
positive_prompt: positivePrompt,
319-
negative_prompt: negativePrompt,
320-
model,
321-
seed,
322-
steps,
323-
rand_device: use_cpu ? 'cpu' : 'cuda',
324-
scheduler,
325-
clip_skip: clipSkip,
326-
strength,
327-
init_image: initialImage.image_name,
328-
});
311+
addCoreMetadataNode(
312+
graph,
313+
{
314+
generation_mode: 'img2img',
315+
cfg_scale,
316+
width: !isUsingScaledDimensions
317+
? width
318+
: scaledBoundingBoxDimensions.width,
319+
height: !isUsingScaledDimensions
320+
? height
321+
: scaledBoundingBoxDimensions.height,
322+
positive_prompt: positivePrompt,
323+
negative_prompt: negativePrompt,
324+
model,
325+
seed,
326+
steps,
327+
rand_device: use_cpu ? 'cpu' : 'cuda',
328+
scheduler,
329+
clip_skip: clipSkip,
330+
strength,
331+
init_image: initialImage.image_name,
332+
},
333+
CANVAS_OUTPUT
334+
);
329335

330336
// Add Seamless To Graph
331337
if (seamlessXAxis || seamlessYAxis) {
@@ -357,7 +363,7 @@ export const buildCanvasImageToImageGraph = (
357363
addWatermarkerToGraph(state, graph, CANVAS_OUTPUT);
358364
}
359365

360-
addSaveImageNode(state, graph);
366+
addLinearUIOutputNode(state, graph);
361367

362368
return graph;
363369
};

invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
1313
import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph';
1414
import { addLoRAsToGraph } from './addLoRAsToGraph';
1515
import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph';
16-
import { addSaveImageNode } from './addSaveImageNode';
16+
import { addLinearUIOutputNode } from './addLinearUIOutputNode';
1717
import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph';
1818
import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph';
1919
import { addVAEToGraph } from './addVAEToGraph';
@@ -666,7 +666,7 @@ export const buildCanvasInpaintGraph = (
666666
addWatermarkerToGraph(state, graph, CANVAS_OUTPUT);
667667
}
668668

669-
addSaveImageNode(state, graph);
669+
addLinearUIOutputNode(state, graph);
670670

671671
return graph;
672672
};

invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasOutpaintGraph.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
1212
import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph';
1313
import { addLoRAsToGraph } from './addLoRAsToGraph';
1414
import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph';
15-
import { addSaveImageNode } from './addSaveImageNode';
15+
import { addLinearUIOutputNode } from './addLinearUIOutputNode';
1616
import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph';
1717
import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph';
1818
import { addVAEToGraph } from './addVAEToGraph';
@@ -770,7 +770,7 @@ export const buildCanvasOutpaintGraph = (
770770
addWatermarkerToGraph(state, graph, CANVAS_OUTPUT);
771771
}
772772

773-
addSaveImageNode(state, graph);
773+
addLinearUIOutputNode(state, graph);
774774

775775
return graph;
776776
};

invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLImageToImageGraph.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph';
77
import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph';
88
import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph';
99
import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph';
10-
import { addSaveImageNode } from './addSaveImageNode';
10+
import { addLinearUIOutputNode } from './addLinearUIOutputNode';
1111
import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph';
1212
import { addVAEToGraph } from './addVAEToGraph';
1313
import { addWatermarkerToGraph } from './addWatermarkerToGraph';
@@ -319,23 +319,29 @@ export const buildCanvasSDXLImageToImageGraph = (
319319
});
320320
}
321321

322-
addCoreMetadataNode(graph, {
323-
generation_mode: 'img2img',
324-
cfg_scale,
325-
width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width,
326-
height: !isUsingScaledDimensions
327-
? height
328-
: scaledBoundingBoxDimensions.height,
329-
positive_prompt: positivePrompt,
330-
negative_prompt: negativePrompt,
331-
model,
332-
seed,
333-
steps,
334-
rand_device: use_cpu ? 'cpu' : 'cuda',
335-
scheduler,
336-
strength,
337-
init_image: initialImage.image_name,
338-
});
322+
addCoreMetadataNode(
323+
graph,
324+
{
325+
generation_mode: 'img2img',
326+
cfg_scale,
327+
width: !isUsingScaledDimensions
328+
? width
329+
: scaledBoundingBoxDimensions.width,
330+
height: !isUsingScaledDimensions
331+
? height
332+
: scaledBoundingBoxDimensions.height,
333+
positive_prompt: positivePrompt,
334+
negative_prompt: negativePrompt,
335+
model,
336+
seed,
337+
steps,
338+
rand_device: use_cpu ? 'cpu' : 'cuda',
339+
scheduler,
340+
strength,
341+
init_image: initialImage.image_name,
342+
},
343+
CANVAS_OUTPUT
344+
);
339345

340346
// Add Seamless To Graph
341347
if (seamlessXAxis || seamlessYAxis) {
@@ -380,7 +386,7 @@ export const buildCanvasSDXLImageToImageGraph = (
380386
addWatermarkerToGraph(state, graph, CANVAS_OUTPUT);
381387
}
382388

383-
addSaveImageNode(state, graph);
389+
addLinearUIOutputNode(state, graph);
384390

385391
return graph;
386392
};

0 commit comments

Comments
 (0)