feat(nodes): add WithBoard field helper class

This class works the same way as `WithMetadata` - it simply adds a `board` field to the node. The context wrapper function is able to pull the board id from this. This allows image-outputting nodes to get a board field "for free", and have their outputs automatically saved to it.

This is a breaking change for node authors who may have a field called `board`, because it makes `board` a reserved field name. I'll look into how to avoid this - maybe by naming this invoke-managed field `_board` to avoid collisions?

Supporting changes:
- `WithBoard` is added to all image-outputting nodes, giving them the ability to save to board.
- Unused, duplicate `WithMetadata` and `WithWorkflow` classes are deleted from `baseinvocation.py`. The "real" versions are in `fields.py`.
- Remove `LinearUIOutputInvocation`. Now that all nodes that output images also have a `board` field by default, this node is no longer necessary. See comment here for context: https://github.com/invoke-ai/InvokeAI/pull/5491#discussion_r1480760629
- Without `LinearUIOutputInvocation`, the `ImagesInferface.update` method is no longer needed, and removed.

Note: This commit does not bump all node versions. I will ensure that is done correctly before merging the PR of which this commit is a part.

Note: A followup commit will implement the frontend changes to support this change.
This commit is contained in:
psychedelicious 2024-02-07 16:33:55 +11:00 committed by Brandon Rising
parent d6ce901ad2
commit 1e4b953ccd
12 changed files with 78 additions and 134 deletions

View File

@ -17,11 +17,8 @@ from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefined
from invokeai.app.invocations.fields import (
FieldDescriptions,
FieldKind,
Input,
InputFieldJSONSchemaExtra,
MetadataField,
)
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.shared.invocation_context import InvocationContext
@ -306,9 +303,7 @@ RESERVED_NODE_ATTRIBUTE_FIELD_NAMES = {
"workflow",
}
RESERVED_INPUT_FIELD_NAMES = {
"metadata",
}
RESERVED_INPUT_FIELD_NAMES = {"metadata", "board"}
RESERVED_OUTPUT_FIELD_NAMES = {"type"}
@ -518,29 +513,3 @@ def invocation_output(
return cls
return wrapper
class WithMetadata(BaseModel):
"""
Inherit from this class if your node needs a metadata input field.
"""
metadata: Optional[MetadataField] = Field(
default=None,
description=FieldDescriptions.metadata,
json_schema_extra=InputFieldJSONSchemaExtra(
field_kind=FieldKind.Internal,
input=Input.Connection,
orig_required=False,
).model_dump(exclude_none=True),
)
class WithWorkflow:
workflow = None
def __init_subclass__(cls) -> None:
logger.warn(
f"{cls.__module__.split('.')[0]}.{cls.__name__}: WithWorkflow is deprecated. Use `context.workflow` to access the workflow."
)
super().__init_subclass__()

View File

@ -25,7 +25,15 @@ from controlnet_aux.util import HWC3, ade_palette
from PIL import Image
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField, WithMetadata
from invokeai.app.invocations.fields import (
FieldDescriptions,
ImageField,
Input,
InputField,
OutputField,
WithBoard,
WithMetadata,
)
from invokeai.app.invocations.primitives import ImageOutput
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
from invokeai.app.services.shared.invocation_context import InvocationContext
@ -135,7 +143,7 @@ class ControlNetInvocation(BaseInvocation):
# This invocation exists for other invocations to subclass it - do not register with @invocation!
class ImageProcessorInvocation(BaseInvocation, WithMetadata):
class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Base class for invocations that preprocess images for ControlNet"""
image: ImageField = InputField(description="The image to process")

View File

@ -10,11 +10,11 @@ from invokeai.app.invocations.primitives import ImageOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from .baseinvocation import BaseInvocation, invocation
from .fields import InputField, WithMetadata
from .fields import InputField, WithBoard, WithMetadata
@invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.2.1")
class CvInpaintInvocation(BaseInvocation, WithMetadata):
class CvInpaintInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Simple inpaint using opencv."""
image: ImageField = InputField(description="The image to inpaint")

View File

@ -16,7 +16,7 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import ImageField, InputField, OutputField, WithMetadata
from invokeai.app.invocations.fields import ImageField, InputField, OutputField, WithBoard, WithMetadata
from invokeai.app.invocations.primitives import ImageOutput
from invokeai.app.services.image_records.image_records_common import ImageCategory
from invokeai.app.services.shared.invocation_context import InvocationContext
@ -619,7 +619,7 @@ class FaceMaskInvocation(BaseInvocation, WithMetadata):
@invocation(
"face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.2.1"
)
class FaceIdentifierInvocation(BaseInvocation, WithMetadata):
class FaceIdentifierInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Outputs an image with detected face IDs printed on each face. For use with other FaceTools."""
image: ImageField = InputField(description="Image to face detect")

View File

@ -280,6 +280,22 @@ class WithWorkflow:
super().__init_subclass__()
class WithBoard(BaseModel):
"""
Inherit from this class if your node needs a board input field.
"""
board: Optional["BoardField"] = Field(
default=None,
description=FieldDescriptions.board,
json_schema_extra=InputFieldJSONSchemaExtra(
field_kind=FieldKind.Internal,
input=Input.Direct,
orig_required=False,
).model_dump(exclude_none=True),
)
class OutputFieldJSONSchemaExtra(BaseModel):
"""
Extra attributes to be added to input fields and their OpenAPI schema. Used by the workflow editor

View File

@ -8,12 +8,11 @@ import numpy
from PIL import Image, ImageChops, ImageFilter, ImageOps
from invokeai.app.invocations.fields import (
BoardField,
ColorField,
FieldDescriptions,
ImageField,
Input,
InputField,
WithBoard,
WithMetadata,
)
from invokeai.app.invocations.primitives import ImageOutput
@ -55,7 +54,7 @@ class ShowImageInvocation(BaseInvocation):
category="image",
version="1.2.1",
)
class BlankImageInvocation(BaseInvocation, WithMetadata):
class BlankImageInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Creates a blank image and forwards it to the pipeline"""
width: int = InputField(default=512, description="The width of the image")
@ -78,7 +77,7 @@ class BlankImageInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ImageCropInvocation(BaseInvocation, WithMetadata):
class ImageCropInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Crops an image to a specified box. The box can be outside of the image."""
image: ImageField = InputField(description="The image to crop")
@ -149,7 +148,7 @@ class CenterPadCropInvocation(BaseInvocation):
category="image",
version="1.2.1",
)
class ImagePasteInvocation(BaseInvocation, WithMetadata):
class ImagePasteInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Pastes an image into another image."""
base_image: ImageField = InputField(description="The base image")
@ -196,7 +195,7 @@ class ImagePasteInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class MaskFromAlphaInvocation(BaseInvocation, WithMetadata):
class MaskFromAlphaInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Extracts the alpha channel of an image as a mask."""
image: ImageField = InputField(description="The image to create the mask from")
@ -221,7 +220,7 @@ class MaskFromAlphaInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ImageMultiplyInvocation(BaseInvocation, WithMetadata):
class ImageMultiplyInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Multiplies two images together using `PIL.ImageChops.multiply()`."""
image1: ImageField = InputField(description="The first image to multiply")
@ -248,7 +247,7 @@ IMAGE_CHANNELS = Literal["A", "R", "G", "B"]
category="image",
version="1.2.1",
)
class ImageChannelInvocation(BaseInvocation, WithMetadata):
class ImageChannelInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Gets a channel from an image."""
image: ImageField = InputField(description="The image to get the channel from")
@ -274,7 +273,7 @@ IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F
category="image",
version="1.2.1",
)
class ImageConvertInvocation(BaseInvocation, WithMetadata):
class ImageConvertInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Converts an image to a different mode."""
image: ImageField = InputField(description="The image to convert")
@ -297,7 +296,7 @@ class ImageConvertInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ImageBlurInvocation(BaseInvocation, WithMetadata):
class ImageBlurInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Blurs an image"""
image: ImageField = InputField(description="The image to blur")
@ -326,7 +325,7 @@ class ImageBlurInvocation(BaseInvocation, WithMetadata):
version="1.2.1",
classification=Classification.Beta,
)
class UnsharpMaskInvocation(BaseInvocation, WithMetadata):
class UnsharpMaskInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Applies an unsharp mask filter to an image"""
image: ImageField = InputField(description="The image to use")
@ -394,7 +393,7 @@ PIL_RESAMPLING_MAP = {
category="image",
version="1.2.1",
)
class ImageResizeInvocation(BaseInvocation, WithMetadata):
class ImageResizeInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Resizes an image to specific dimensions"""
image: ImageField = InputField(description="The image to resize")
@ -424,7 +423,7 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ImageScaleInvocation(BaseInvocation, WithMetadata):
class ImageScaleInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Scales an image by a factor"""
image: ImageField = InputField(description="The image to scale")
@ -459,7 +458,7 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ImageLerpInvocation(BaseInvocation, WithMetadata):
class ImageLerpInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Linear interpolation of all pixels of an image"""
image: ImageField = InputField(description="The image to lerp")
@ -486,7 +485,7 @@ class ImageLerpInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ImageInverseLerpInvocation(BaseInvocation, WithMetadata):
class ImageInverseLerpInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Inverse linear interpolation of all pixels of an image"""
image: ImageField = InputField(description="The image to lerp")
@ -513,7 +512,7 @@ class ImageInverseLerpInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata):
class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Add blur to NSFW-flagged images"""
image: ImageField = InputField(description="The image to check")
@ -548,7 +547,7 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ImageWatermarkInvocation(BaseInvocation, WithMetadata):
class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Add an invisible watermark to an image"""
image: ImageField = InputField(description="The image to check")
@ -569,7 +568,7 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class MaskEdgeInvocation(BaseInvocation, WithMetadata):
class MaskEdgeInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Applies an edge mask to an image"""
image: ImageField = InputField(description="The image to apply the mask to")
@ -608,7 +607,7 @@ class MaskEdgeInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class MaskCombineInvocation(BaseInvocation, WithMetadata):
class MaskCombineInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`."""
mask1: ImageField = InputField(description="The first mask to combine")
@ -632,7 +631,7 @@ class MaskCombineInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ColorCorrectInvocation(BaseInvocation, WithMetadata):
class ColorCorrectInvocation(BaseInvocation, WithMetadata, WithBoard):
"""
Shifts the colors of a target image to match the reference image, optionally
using a mask to only color-correct certain regions of the target image.
@ -736,7 +735,7 @@ class ColorCorrectInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ImageHueAdjustmentInvocation(BaseInvocation, WithMetadata):
class ImageHueAdjustmentInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Adjusts the Hue of an image."""
image: ImageField = InputField(description="The image to adjust")
@ -825,7 +824,7 @@ CHANNEL_FORMATS = {
category="image",
version="1.2.1",
)
class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata):
class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Add or subtract a value from a specific color channel of an image."""
image: ImageField = InputField(description="The image to adjust")
@ -881,7 +880,7 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata):
category="image",
version="1.2.1",
)
class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata):
class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Scale a specific color channel of an image."""
image: ImageField = InputField(description="The image to adjust")
@ -926,41 +925,14 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata):
version="1.2.1",
use_cache=False,
)
class SaveImageInvocation(BaseInvocation, WithMetadata):
class SaveImageInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Saves an image. Unlike an image primitive, this invocation stores a copy of the image."""
image: ImageField = InputField(description=FieldDescriptions.image)
board: BoardField = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct)
def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.images.get_pil(self.image.image_name)
image_dto = context.images.save(image=image, board_id=self.board.board_id if self.board else None)
return ImageOutput.build(image_dto)
@invocation(
"linear_ui_output",
title="Linear UI Image Output",
tags=["primitives", "image"],
category="primitives",
version="1.0.2",
use_cache=False,
)
class LinearUIOutputInvocation(BaseInvocation, WithMetadata):
"""Handles Linear UI Image Outputting tasks."""
image: ImageField = InputField(description=FieldDescriptions.image)
board: Optional[BoardField] = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct)
def invoke(self, context: InvocationContext) -> ImageOutput:
image_dto = context.images.get_dto(self.image.image_name)
image_dto = context.images.update(
image_name=self.image.image_name,
board_id=self.board.board_id if self.board else None,
is_intermediate=self.is_intermediate,
)
image_dto = context.images.save(image=image)
return ImageOutput.build(image_dto)

View File

@ -15,7 +15,7 @@ from invokeai.backend.image_util.lama import LaMA
from invokeai.backend.image_util.patchmatch import PatchMatch
from .baseinvocation import BaseInvocation, invocation
from .fields import InputField, WithMetadata
from .fields import InputField, WithBoard, WithMetadata
from .image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES
@ -121,7 +121,7 @@ def tile_fill_missing(im: Image.Image, tile_size: int = 16, seed: Optional[int]
@invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1")
class InfillColorInvocation(BaseInvocation, WithMetadata):
class InfillColorInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Infills transparent areas of an image with a solid color"""
image: ImageField = InputField(description="The image to infill")
@ -144,7 +144,7 @@ class InfillColorInvocation(BaseInvocation, WithMetadata):
@invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.2")
class InfillTileInvocation(BaseInvocation, WithMetadata):
class InfillTileInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Infills transparent areas of an image with tiles of the image"""
image: ImageField = InputField(description="The image to infill")
@ -170,7 +170,7 @@ class InfillTileInvocation(BaseInvocation, WithMetadata):
@invocation(
"infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1"
)
class InfillPatchMatchInvocation(BaseInvocation, WithMetadata):
class InfillPatchMatchInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Infills transparent areas of an image using the PatchMatch algorithm"""
image: ImageField = InputField(description="The image to infill")
@ -209,7 +209,7 @@ class InfillPatchMatchInvocation(BaseInvocation, WithMetadata):
@invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1")
class LaMaInfillInvocation(BaseInvocation, WithMetadata):
class LaMaInfillInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Infills transparent areas of an image using the LaMa model"""
image: ImageField = InputField(description="The image to infill")
@ -225,7 +225,7 @@ class LaMaInfillInvocation(BaseInvocation, WithMetadata):
@invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1")
class CV2InfillInvocation(BaseInvocation, WithMetadata):
class CV2InfillInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Infills transparent areas of an image using OpenCV Inpainting"""
image: ImageField = InputField(description="The image to infill")

View File

@ -33,6 +33,7 @@ from invokeai.app.invocations.fields import (
LatentsField,
OutputField,
UIType,
WithBoard,
WithMetadata,
)
from invokeai.app.invocations.ip_adapter import IPAdapterField
@ -762,7 +763,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
category="latents",
version="1.2.1",
)
class LatentsToImageInvocation(BaseInvocation, WithMetadata):
class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Generates an image from latents."""
latents: LatentsField = InputField(

View File

@ -255,9 +255,7 @@ class ImageCollectionOutput(BaseInvocationOutput):
@invocation("image", title="Image Primitive", tags=["primitives", "image"], category="primitives", version="1.0.1")
class ImageInvocation(
BaseInvocation,
):
class ImageInvocation(BaseInvocation):
"""An image primitive value"""
image: ImageField = InputField(description="The image to load")

View File

@ -11,7 +11,7 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import ImageField, Input, InputField, OutputField, WithMetadata
from invokeai.app.invocations.fields import ImageField, Input, InputField, OutputField, WithBoard, WithMetadata
from invokeai.app.invocations.primitives import ImageOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.tiles.tiles import (
@ -232,7 +232,7 @@ BLEND_MODES = Literal["Linear", "Seam"]
version="1.1.0",
classification=Classification.Beta,
)
class MergeTilesToImageInvocation(BaseInvocation, WithMetadata):
class MergeTilesToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Merge multiple tile images into a single image."""
# Inputs

View File

@ -16,7 +16,7 @@ from invokeai.backend.image_util.realesrgan.realesrgan import RealESRGAN
from invokeai.backend.util.devices import choose_torch_device
from .baseinvocation import BaseInvocation, invocation
from .fields import InputField, WithMetadata
from .fields import InputField, WithBoard, WithMetadata
# TODO: Populate this from disk?
# TODO: Use model manager to load?
@ -32,7 +32,7 @@ if choose_torch_device() == torch.device("mps"):
@invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="esrgan", version="1.3.1")
class ESRGANInvocation(BaseInvocation, WithMetadata):
class ESRGANInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Upscales an image using RealESRGAN."""
image: ImageField = InputField(description="The input image")

View File

@ -5,10 +5,10 @@ from deprecated import deprecated
from PIL.Image import Image
from torch import Tensor
from invokeai.app.invocations.fields import MetadataField, WithMetadata
from invokeai.app.invocations.fields import MetadataField, WithBoard, WithMetadata
from invokeai.app.services.boards.boards_common import BoardDTO
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
from invokeai.app.services.images.images_common import ImageDTO
from invokeai.app.services.invocation_services import InvocationServices
from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID
@ -158,7 +158,9 @@ class ImagesInterface(InvocationContextInterface):
If the current queue item has a workflow or metadata, it is automatically saved with the image.
:param image: The image to save, as a PIL image.
:param board_id: The board ID to add the image to, if it should be added.
:param board_id: The board ID to add the image to, if it should be added. It the invocation \
inherits from `WithBoard`, that board will be used automatically. **Use this only if \
you want to override or provide a board manually!**
:param image_category: The category of the image. Only the GENERAL category is added \
to the gallery.
:param metadata: The metadata to save with the image, if it should have any. If the \
@ -173,11 +175,15 @@ class ImagesInterface(InvocationContextInterface):
else metadata
)
# If the invocation inherits WithBoard, use that. Else, use the board_id passed in.
board_ = self._context_data.invocation.board if isinstance(self._context_data.invocation, WithBoard) else None
board_id_ = board_.board_id if board_ is not None else board_id
return self._services.images.create(
image=image,
is_intermediate=self._context_data.invocation.is_intermediate,
image_category=image_category,
board_id=board_id,
board_id=board_id_,
metadata=metadata_,
image_origin=ResourceOrigin.INTERNAL,
workflow=self._context_data.workflow,
@ -209,32 +215,6 @@ class ImagesInterface(InvocationContextInterface):
"""
return self._services.images.get_dto(image_name)
def update(
self,
image_name: str,
board_id: Optional[str] = None,
is_intermediate: Optional[bool] = False,
) -> ImageDTO:
"""
Updates an image, returning its updated DTO.
It is not suggested to update images saved by earlier nodes, as this can cause confusion for users.
If you use this method, you *must* return the image as an :class:`ImageOutput` for the gallery to
get the updated image.
:param image_name: The name of the image to update.
:param board_id: The board ID to add the image to, if it should be added.
:param is_intermediate: Whether the image is an intermediate. Intermediate images aren't added to the gallery.
"""
if is_intermediate is not None:
self._services.images.update(image_name, ImageRecordChanges(is_intermediate=is_intermediate))
if board_id is None:
self._services.board_images.remove_image_from_board(image_name)
else:
self._services.board_images.add_image_to_board(image_name, board_id)
return self._services.images.get_dto(image_name)
class LatentsInterface(InvocationContextInterface):
def save(self, tensor: Tensor) -> str: