Merge branch 'main' into fix-inpainting

This commit is contained in:
blessedcoolant 2023-07-13 20:42:13 +12:00
commit 5a546e66f1
73 changed files with 2315 additions and 1517 deletions

View File

@ -13,7 +13,6 @@ from invokeai.app.services.board_record_storage import SqliteBoardRecordStorage
from invokeai.app.services.boards import BoardService, BoardServiceDependencies from invokeai.app.services.boards import BoardService, BoardServiceDependencies
from invokeai.app.services.image_record_storage import SqliteImageRecordStorage from invokeai.app.services.image_record_storage import SqliteImageRecordStorage
from invokeai.app.services.images import ImageService, ImageServiceDependencies from invokeai.app.services.images import ImageService, ImageServiceDependencies
from invokeai.app.services.metadata import CoreMetadataService
from invokeai.app.services.resource_name import SimpleNameService from invokeai.app.services.resource_name import SimpleNameService
from invokeai.app.services.urls import LocalUrlService from invokeai.app.services.urls import LocalUrlService
from invokeai.backend.util.logging import InvokeAILogger from invokeai.backend.util.logging import InvokeAILogger
@ -75,7 +74,6 @@ class ApiDependencies:
) )
urls = LocalUrlService() urls = LocalUrlService()
metadata = CoreMetadataService()
image_record_storage = SqliteImageRecordStorage(db_location) image_record_storage = SqliteImageRecordStorage(db_location)
image_file_storage = DiskImageFileStorage(f"{output_folder}/images") image_file_storage = DiskImageFileStorage(f"{output_folder}/images")
names = SimpleNameService() names = SimpleNameService()
@ -111,7 +109,6 @@ class ApiDependencies:
board_image_record_storage=board_image_record_storage, board_image_record_storage=board_image_record_storage,
image_record_storage=image_record_storage, image_record_storage=image_record_storage,
image_file_storage=image_file_storage, image_file_storage=image_file_storage,
metadata=metadata,
url=urls, url=urls,
logger=logger, logger=logger,
names=names, names=names,

View File

@ -1,20 +1,19 @@
import io import io
from typing import Optional from typing import Optional
from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadFile
from fastapi.routing import APIRouter from fastapi import (Body, HTTPException, Path, Query, Request, Response,
UploadFile)
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.routing import APIRouter
from PIL import Image from PIL import Image
from invokeai.app.models.image import (
ImageCategory, from invokeai.app.invocations.metadata import ImageMetadata
ResourceOrigin, from invokeai.app.models.image import ImageCategory, ResourceOrigin
)
from invokeai.app.services.image_record_storage import OffsetPaginatedResults from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.models.image_record import (
ImageDTO,
ImageRecordChanges,
ImageUrlsDTO,
)
from invokeai.app.services.item_storage import PaginatedResults from invokeai.app.services.item_storage import PaginatedResults
from invokeai.app.services.models.image_record import (ImageDTO,
ImageRecordChanges,
ImageUrlsDTO)
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies
@ -103,23 +102,38 @@ async def update_image(
@images_router.get( @images_router.get(
"/{image_name}/metadata", "/{image_name}",
operation_id="get_image_metadata", operation_id="get_image_dto",
response_model=ImageDTO, response_model=ImageDTO,
) )
async def get_image_metadata( async def get_image_dto(
image_name: str = Path(description="The name of image to get"), image_name: str = Path(description="The name of image to get"),
) -> ImageDTO: ) -> ImageDTO:
"""Gets an image's metadata""" """Gets an image's DTO"""
try: try:
return ApiDependencies.invoker.services.images.get_dto(image_name) return ApiDependencies.invoker.services.images.get_dto(image_name)
except Exception as e: except Exception as e:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@images_router.get(
"/{image_name}/metadata",
operation_id="get_image_metadata",
response_model=ImageMetadata,
)
async def get_image_metadata(
image_name: str = Path(description="The name of image to get"),
) -> ImageMetadata:
"""Gets an image's metadata"""
try:
return ApiDependencies.invoker.services.images.get_metadata(image_name)
except Exception as e:
raise HTTPException(status_code=404)
@images_router.get( @images_router.get(
"/{image_name}", "/{image_name}/full",
operation_id="get_image_full", operation_id="get_image_full",
response_class=Response, response_class=Response,
responses={ responses={
@ -208,10 +222,10 @@ async def get_image_urls(
@images_router.get( @images_router.get(
"/", "/",
operation_id="list_images_with_metadata", operation_id="list_image_dtos",
response_model=OffsetPaginatedResults[ImageDTO], response_model=OffsetPaginatedResults[ImageDTO],
) )
async def list_images_with_metadata( async def list_image_dtos(
image_origin: Optional[ResourceOrigin] = Query( image_origin: Optional[ResourceOrigin] = Query(
default=None, description="The origin of images to list" default=None, description="The origin of images to list"
), ),
@ -227,7 +241,7 @@ async def list_images_with_metadata(
offset: int = Query(default=0, description="The page offset"), offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of images per page"), limit: int = Query(default=10, description="The number of images per page"),
) -> OffsetPaginatedResults[ImageDTO]: ) -> OffsetPaginatedResults[ImageDTO]:
"""Gets a list of images""" """Gets a list of image DTOs"""
image_dtos = ApiDependencies.invoker.services.images.get_many( image_dtos = ApiDependencies.invoker.services.images.get_many(
offset, offset,

View File

@ -34,7 +34,6 @@ from invokeai.app.services.board_record_storage import SqliteBoardRecordStorage
from invokeai.app.services.boards import BoardService, BoardServiceDependencies from invokeai.app.services.boards import BoardService, BoardServiceDependencies
from invokeai.app.services.image_record_storage import SqliteImageRecordStorage from invokeai.app.services.image_record_storage import SqliteImageRecordStorage
from invokeai.app.services.images import ImageService, ImageServiceDependencies from invokeai.app.services.images import ImageService, ImageServiceDependencies
from invokeai.app.services.metadata import CoreMetadataService
from invokeai.app.services.resource_name import SimpleNameService from invokeai.app.services.resource_name import SimpleNameService
from invokeai.app.services.urls import LocalUrlService from invokeai.app.services.urls import LocalUrlService
from .services.default_graphs import (default_text_to_image_graph_id, from .services.default_graphs import (default_text_to_image_graph_id,
@ -244,7 +243,6 @@ def invoke_cli():
) )
urls = LocalUrlService() urls = LocalUrlService()
metadata = CoreMetadataService()
image_record_storage = SqliteImageRecordStorage(db_location) image_record_storage = SqliteImageRecordStorage(db_location)
image_file_storage = DiskImageFileStorage(f"{output_folder}/images") image_file_storage = DiskImageFileStorage(f"{output_folder}/images")
names = SimpleNameService() names = SimpleNameService()
@ -277,7 +275,6 @@ def invoke_cli():
board_image_record_storage=board_image_record_storage, board_image_record_storage=board_image_record_storage,
image_record_storage=image_record_storage, image_record_storage=image_record_storage,
image_file_storage=image_file_storage, image_file_storage=image_file_storage,
metadata=metadata,
url=urls, url=urls,
logger=logger, logger=logger,
names=names, names=names,

View File

@ -9,9 +9,9 @@ from diffusers.image_processor import VaeImageProcessor
from diffusers.schedulers import SchedulerMixin as Scheduler from diffusers.schedulers import SchedulerMixin as Scheduler
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
from invokeai.app.invocations.metadata import CoreMetadata
from invokeai.app.util.step_callback import stable_diffusion_step_callback from invokeai.app.util.step_callback import stable_diffusion_step_callback
from ..models.image import ImageCategory, ImageField, ResourceOrigin
from ...backend.model_management.lora import ModelPatcher from ...backend.model_management.lora import ModelPatcher
from ...backend.stable_diffusion import PipelineIntermediateState from ...backend.stable_diffusion import PipelineIntermediateState
from ...backend.stable_diffusion.diffusers_pipeline import ( from ...backend.stable_diffusion.diffusers_pipeline import (
@ -21,6 +21,7 @@ from ...backend.stable_diffusion.diffusion.shared_invokeai_diffusion import \
PostprocessingSettings PostprocessingSettings
from ...backend.stable_diffusion.schedulers import SCHEDULER_MAP from ...backend.stable_diffusion.schedulers import SCHEDULER_MAP
from ...backend.util.devices import torch_dtype from ...backend.util.devices import torch_dtype
from ..models.image import ImageCategory, ImageField, ResourceOrigin
from .baseinvocation import (BaseInvocation, BaseInvocationOutput, from .baseinvocation import (BaseInvocation, BaseInvocationOutput,
InvocationConfig, InvocationContext) InvocationConfig, InvocationContext)
from .compel import ConditioningField from .compel import ConditioningField
@ -449,6 +450,8 @@ class LatentsToImageInvocation(BaseInvocation):
tiled: bool = Field( tiled: bool = Field(
default=False, default=False,
description="Decode latents by overlaping tiles(less memory consumption)") description="Decode latents by overlaping tiles(less memory consumption)")
metadata: Optional[CoreMetadata] = Field(default=None, description="Optional core metadata to be written to the image")
# Schema customisation # Schema customisation
class Config(InvocationConfig): class Config(InvocationConfig):
@ -493,7 +496,8 @@ class LatentsToImageInvocation(BaseInvocation):
image_category=ImageCategory.GENERAL, image_category=ImageCategory.GENERAL,
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate is_intermediate=self.is_intermediate,
metadata=self.metadata.dict() if self.metadata else None,
) )
return ImageOutput( return ImageOutput(

View File

@ -0,0 +1,124 @@
from typing import Literal, Optional, Union
from pydantic import BaseModel, Field
from invokeai.app.invocations.baseinvocation import (BaseInvocation,
BaseInvocationOutput,
InvocationContext)
from invokeai.app.invocations.controlnet_image_processors import ControlField
from invokeai.app.invocations.model import (LoRAModelField, MainModelField,
VAEModelField)
class LoRAMetadataField(BaseModel):
"""LoRA metadata for an image generated in InvokeAI."""
lora: LoRAModelField = Field(description="The LoRA model")
weight: float = Field(description="The weight of the LoRA model")
class CoreMetadata(BaseModel):
"""Core generation metadata for an image generated in InvokeAI."""
generation_mode: str = Field(description="The generation mode that output this image",)
positive_prompt: str = Field(description="The positive prompt parameter")
negative_prompt: str = Field(description="The negative prompt parameter")
width: int = Field(description="The width parameter")
height: int = Field(description="The height parameter")
seed: int = Field(description="The seed used for noise generation")
rand_device: str = Field(description="The device used for random number generation")
cfg_scale: float = Field(description="The classifier-free guidance scale parameter")
steps: int = Field(description="The number of steps used for inference")
scheduler: str = Field(description="The scheduler used for inference")
clip_skip: int = Field(description="The number of skipped CLIP layers",)
model: MainModelField = Field(description="The main model used for inference")
controlnets: list[ControlField]= Field(description="The ControlNets used for inference")
loras: list[LoRAMetadataField] = Field(description="The LoRAs used for inference")
strength: Union[float, None] = Field(
default=None,
description="The strength used for latents-to-latents",
)
init_image: Union[str, None] = Field(
default=None, description="The name of the initial image"
)
vae: Union[VAEModelField, None] = Field(
default=None,
description="The VAE used for decoding, if the main model's default was not used",
)
class ImageMetadata(BaseModel):
"""An image's generation metadata"""
metadata: Optional[dict] = Field(
default=None,
description="The image's core metadata, if it was created in the Linear or Canvas UI",
)
graph: Optional[dict] = Field(
default=None, description="The graph that created the image"
)
class MetadataAccumulatorOutput(BaseInvocationOutput):
"""The output of the MetadataAccumulator node"""
type: Literal["metadata_accumulator_output"] = "metadata_accumulator_output"
metadata: CoreMetadata = Field(description="The core metadata for the image")
class MetadataAccumulatorInvocation(BaseInvocation):
"""Outputs a Core Metadata Object"""
type: Literal["metadata_accumulator"] = "metadata_accumulator"
generation_mode: str = Field(description="The generation mode that output this image",)
positive_prompt: str = Field(description="The positive prompt parameter")
negative_prompt: str = Field(description="The negative prompt parameter")
width: int = Field(description="The width parameter")
height: int = Field(description="The height parameter")
seed: int = Field(description="The seed used for noise generation")
rand_device: str = Field(description="The device used for random number generation")
cfg_scale: float = Field(description="The classifier-free guidance scale parameter")
steps: int = Field(description="The number of steps used for inference")
scheduler: str = Field(description="The scheduler used for inference")
clip_skip: int = Field(description="The number of skipped CLIP layers",)
model: MainModelField = Field(description="The main model used for inference")
controlnets: list[ControlField]= Field(description="The ControlNets used for inference")
loras: list[LoRAMetadataField] = Field(description="The LoRAs used for inference")
strength: Union[float, None] = Field(
default=None,
description="The strength used for latents-to-latents",
)
init_image: Union[str, None] = Field(
default=None, description="The name of the initial image"
)
vae: Union[VAEModelField, None] = Field(
default=None,
description="The VAE used for decoding, if the main model's default was not used",
)
def invoke(self, context: InvocationContext) -> MetadataAccumulatorOutput:
"""Collects and outputs a CoreMetadata object"""
return MetadataAccumulatorOutput(
metadata=CoreMetadata(
generation_mode=self.generation_mode,
positive_prompt=self.positive_prompt,
negative_prompt=self.negative_prompt,
width=self.width,
height=self.height,
seed=self.seed,
rand_device=self.rand_device,
cfg_scale=self.cfg_scale,
steps=self.steps,
scheduler=self.scheduler,
model=self.model,
strength=self.strength,
init_image=self.init_image,
vae=self.vae,
controlnets=self.controlnets,
loras=self.loras,
clip_skip=self.clip_skip,
)
)

View File

@ -1,93 +0,0 @@
from typing import Optional, Union, List
from pydantic import BaseModel, Extra, Field, StrictFloat, StrictInt, StrictStr
class ImageMetadata(BaseModel):
"""
Core generation metadata for an image/tensor generated in InvokeAI.
Also includes any metadata from the image's PNG tEXt chunks.
Generated by traversing the execution graph, collecting the parameters of the nearest ancestors
of a given node.
Full metadata may be accessed by querying for the session in the `graph_executions` table.
"""
class Config:
extra = Extra.allow
"""
This lets the ImageMetadata class accept arbitrary additional fields. The CoreMetadataService
won't add any fields that are not already defined, but other a different metadata service
implementation might.
"""
type: Optional[StrictStr] = Field(
default=None,
description="The type of the ancestor node of the image output node.",
)
"""The type of the ancestor node of the image output node."""
positive_conditioning: Optional[StrictStr] = Field(
default=None, description="The positive conditioning."
)
"""The positive conditioning"""
negative_conditioning: Optional[StrictStr] = Field(
default=None, description="The negative conditioning."
)
"""The negative conditioning"""
width: Optional[StrictInt] = Field(
default=None, description="Width of the image/latents in pixels."
)
"""Width of the image/latents in pixels"""
height: Optional[StrictInt] = Field(
default=None, description="Height of the image/latents in pixels."
)
"""Height of the image/latents in pixels"""
seed: Optional[StrictInt] = Field(
default=None, description="The seed used for noise generation."
)
"""The seed used for noise generation"""
# cfg_scale: Optional[StrictFloat] = Field(
# cfg_scale: Union[float, list[float]] = Field(
cfg_scale: Union[StrictFloat, List[StrictFloat]] = Field(
default=None, description="The classifier-free guidance scale."
)
"""The classifier-free guidance scale"""
steps: Optional[StrictInt] = Field(
default=None, description="The number of steps used for inference."
)
"""The number of steps used for inference"""
scheduler: Optional[StrictStr] = Field(
default=None, description="The scheduler used for inference."
)
"""The scheduler used for inference"""
model: Optional[StrictStr] = Field(
default=None, description="The model used for inference."
)
"""The model used for inference"""
strength: Optional[StrictFloat] = Field(
default=None,
description="The strength used for image-to-image/latents-to-latents.",
)
"""The strength used for image-to-image/latents-to-latents."""
latents: Optional[StrictStr] = Field(
default=None, description="The ID of the initial latents."
)
"""The ID of the initial latents"""
vae: Optional[StrictStr] = Field(
default=None, description="The VAE used for decoding."
)
"""The VAE used for decoding"""
unet: Optional[StrictStr] = Field(
default=None, description="The UNet used dor inference."
)
"""The UNet used dor inference"""
clip: Optional[StrictStr] = Field(
default=None, description="The CLIP Encoder used for conditioning."
)
"""The CLIP Encoder used for conditioning"""
extra: Optional[StrictStr] = Field(
default=None,
description="Uploaded image metadata, extracted from the PNG tEXt chunk.",
)
"""Uploaded image metadata, extracted from the PNG tEXt chunk."""

View File

@ -1,14 +1,14 @@
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team
import json
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from queue import Queue from queue import Queue
from typing import Dict, Optional, Union from typing import Dict, Optional, Union
from PIL.Image import Image as PILImageType
from PIL import Image, PngImagePlugin from PIL import Image, PngImagePlugin
from PIL.Image import Image as PILImageType
from send2trash import send2trash from send2trash import send2trash
from invokeai.app.models.metadata import ImageMetadata
from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail
@ -59,7 +59,8 @@ class ImageFileStorageBase(ABC):
self, self,
image: PILImageType, image: PILImageType,
image_name: str, image_name: str,
metadata: Optional[ImageMetadata] = None, metadata: Optional[dict] = None,
graph: Optional[dict] = None,
thumbnail_size: int = 256, thumbnail_size: int = 256,
) -> None: ) -> None:
"""Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image name, thumbnail name, and created timestamp.""" """Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image name, thumbnail name, and created timestamp."""
@ -110,20 +111,22 @@ class DiskImageFileStorage(ImageFileStorageBase):
self, self,
image: PILImageType, image: PILImageType,
image_name: str, image_name: str,
metadata: Optional[ImageMetadata] = None, metadata: Optional[dict] = None,
graph: Optional[dict] = None,
thumbnail_size: int = 256, thumbnail_size: int = 256,
) -> None: ) -> None:
try: try:
self.__validate_storage_folders() self.__validate_storage_folders()
image_path = self.get_path(image_name) image_path = self.get_path(image_name)
pnginfo = PngImagePlugin.PngInfo()
if metadata is not None: if metadata is not None:
pnginfo = PngImagePlugin.PngInfo() pnginfo.add_text("invokeai_metadata", json.dumps(metadata))
pnginfo.add_text("invokeai", metadata.json()) if graph is not None:
image.save(image_path, "PNG", pnginfo=pnginfo) pnginfo.add_text("invokeai_graph", json.dumps(graph))
else:
image.save(image_path, "PNG")
image.save(image_path, "PNG", pnginfo=pnginfo)
thumbnail_name = get_thumbnail_name(image_name) thumbnail_name = get_thumbnail_name(image_name)
thumbnail_path = self.get_path(thumbnail_name, thumbnail=True) thumbnail_path = self.get_path(thumbnail_name, thumbnail=True)
thumbnail_image = make_thumbnail(image, thumbnail_size) thumbnail_image = make_thumbnail(image, thumbnail_size)

View File

@ -1,3 +1,4 @@
import json
import sqlite3 import sqlite3
import threading import threading
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@ -8,7 +9,6 @@ from pydantic import BaseModel, Field
from pydantic.generics import GenericModel from pydantic.generics import GenericModel
from invokeai.app.models.image import ImageCategory, ResourceOrigin from invokeai.app.models.image import ImageCategory, ResourceOrigin
from invokeai.app.models.metadata import ImageMetadata
from invokeai.app.services.models.image_record import ( from invokeai.app.services.models.image_record import (
ImageRecord, ImageRecordChanges, deserialize_image_record) ImageRecord, ImageRecordChanges, deserialize_image_record)
@ -48,6 +48,28 @@ class ImageRecordDeleteException(Exception):
super().__init__(message) super().__init__(message)
IMAGE_DTO_COLS = ", ".join(
list(
map(
lambda c: "images." + c,
[
"image_name",
"image_origin",
"image_category",
"width",
"height",
"session_id",
"node_id",
"is_intermediate",
"created_at",
"updated_at",
"deleted_at",
],
)
)
)
class ImageRecordStorageBase(ABC): class ImageRecordStorageBase(ABC):
"""Low-level service responsible for interfacing with the image record store.""" """Low-level service responsible for interfacing with the image record store."""
@ -58,6 +80,11 @@ class ImageRecordStorageBase(ABC):
"""Gets an image record.""" """Gets an image record."""
pass pass
@abstractmethod
def get_metadata(self, image_name: str) -> Optional[dict]:
"""Gets an image's metadata'."""
pass
@abstractmethod @abstractmethod
def update( def update(
self, self,
@ -102,7 +129,7 @@ class ImageRecordStorageBase(ABC):
height: int, height: int,
session_id: Optional[str], session_id: Optional[str],
node_id: Optional[str], node_id: Optional[str],
metadata: Optional[ImageMetadata], metadata: Optional[dict],
is_intermediate: bool = False, is_intermediate: bool = False,
) -> datetime: ) -> datetime:
"""Saves an image record.""" """Saves an image record."""
@ -206,7 +233,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
self._cursor.execute( self._cursor.execute(
f"""--sql f"""--sql
SELECT * FROM images SELECT {IMAGE_DTO_COLS} FROM images
WHERE image_name = ?; WHERE image_name = ?;
""", """,
(image_name,), (image_name,),
@ -224,6 +251,28 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
return deserialize_image_record(dict(result)) return deserialize_image_record(dict(result))
def get_metadata(self, image_name: str) -> Optional[dict]:
try:
self._lock.acquire()
self._cursor.execute(
f"""--sql
SELECT images.metadata FROM images
WHERE image_name = ?;
""",
(image_name,),
)
result = cast(Optional[sqlite3.Row], self._cursor.fetchone())
if not result or not result[0]:
return None
return json.loads(result[0])
except sqlite3.Error as e:
self._conn.rollback()
raise ImageRecordNotFoundException from e
finally:
self._lock.release()
def update( def update(
self, self,
image_name: str, image_name: str,
@ -291,8 +340,8 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
WHERE 1=1 WHERE 1=1
""" """
images_query = """--sql images_query = f"""--sql
SELECT images.* SELECT {IMAGE_DTO_COLS}
FROM images FROM images
LEFT JOIN board_images ON board_images.image_name = images.image_name LEFT JOIN board_images ON board_images.image_name = images.image_name
WHERE 1=1 WHERE 1=1
@ -410,12 +459,12 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
width: int, width: int,
height: int, height: int,
node_id: Optional[str], node_id: Optional[str],
metadata: Optional[ImageMetadata], metadata: Optional[dict],
is_intermediate: bool = False, is_intermediate: bool = False,
) -> datetime: ) -> datetime:
try: try:
metadata_json = ( metadata_json = (
None if metadata is None else metadata.json(exclude_none=True) None if metadata is None else json.dumps(metadata)
) )
self._lock.acquire() self._lock.acquire()
self._cursor.execute( self._cursor.execute(
@ -465,9 +514,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
finally: finally:
self._lock.release() self._lock.release()
def get_most_recent_image_for_board( def get_most_recent_image_for_board(self, board_id: str) -> Optional[ImageRecord]:
self, board_id: str
) -> Optional[ImageRecord]:
try: try:
self._lock.acquire() self._lock.acquire()
self._cursor.execute( self._cursor.execute(

View File

@ -1,39 +1,30 @@
import json
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from logging import Logger from logging import Logger
from typing import Optional, TYPE_CHECKING, Union from typing import TYPE_CHECKING, Optional
from PIL.Image import Image as PILImageType from PIL.Image import Image as PILImageType
from invokeai.app.models.image import ( from invokeai.app.invocations.metadata import ImageMetadata
ImageCategory, from invokeai.app.models.image import (ImageCategory,
ResourceOrigin, InvalidImageCategoryException,
InvalidImageCategoryException, InvalidOriginException, ResourceOrigin)
InvalidOriginException, from invokeai.app.services.board_image_record_storage import \
) BoardImageRecordStorageBase
from invokeai.app.models.metadata import ImageMetadata from invokeai.app.services.graph import Graph
from invokeai.app.services.board_image_record_storage import BoardImageRecordStorageBase
from invokeai.app.services.image_record_storage import (
ImageRecordDeleteException,
ImageRecordNotFoundException,
ImageRecordSaveException,
ImageRecordStorageBase,
OffsetPaginatedResults,
)
from invokeai.app.services.models.image_record import (
ImageRecord,
ImageDTO,
ImageRecordChanges,
image_record_to_dto,
)
from invokeai.app.services.image_file_storage import ( from invokeai.app.services.image_file_storage import (
ImageFileDeleteException, ImageFileDeleteException, ImageFileNotFoundException,
ImageFileNotFoundException, ImageFileSaveException, ImageFileStorageBase)
ImageFileSaveException, from invokeai.app.services.image_record_storage import (
ImageFileStorageBase, ImageRecordDeleteException, ImageRecordNotFoundException,
) ImageRecordSaveException, ImageRecordStorageBase, OffsetPaginatedResults)
from invokeai.app.services.item_storage import ItemStorageABC, PaginatedResults from invokeai.app.services.item_storage import ItemStorageABC
from invokeai.app.services.metadata import MetadataServiceBase from invokeai.app.services.models.image_record import (ImageDTO, ImageRecord,
ImageRecordChanges,
image_record_to_dto)
from invokeai.app.services.resource_name import NameServiceBase from invokeai.app.services.resource_name import NameServiceBase
from invokeai.app.services.urls import UrlServiceBase from invokeai.app.services.urls import UrlServiceBase
from invokeai.app.util.metadata import get_metadata_graph_from_raw_session
if TYPE_CHECKING: if TYPE_CHECKING:
from invokeai.app.services.graph import GraphExecutionState from invokeai.app.services.graph import GraphExecutionState
@ -51,6 +42,7 @@ class ImageServiceABC(ABC):
node_id: Optional[str] = None, node_id: Optional[str] = None,
session_id: Optional[str] = None, session_id: Optional[str] = None,
is_intermediate: bool = False, is_intermediate: bool = False,
metadata: Optional[dict] = None,
) -> ImageDTO: ) -> ImageDTO:
"""Creates an image, storing the file and its metadata.""" """Creates an image, storing the file and its metadata."""
pass pass
@ -79,6 +71,11 @@ class ImageServiceABC(ABC):
"""Gets an image DTO.""" """Gets an image DTO."""
pass pass
@abstractmethod
def get_metadata(self, image_name: str) -> ImageMetadata:
"""Gets an image's metadata."""
pass
@abstractmethod @abstractmethod
def get_path(self, image_name: str, thumbnail: bool = False) -> str: def get_path(self, image_name: str, thumbnail: bool = False) -> str:
"""Gets an image's path.""" """Gets an image's path."""
@ -124,7 +121,6 @@ class ImageServiceDependencies:
image_records: ImageRecordStorageBase image_records: ImageRecordStorageBase
image_files: ImageFileStorageBase image_files: ImageFileStorageBase
board_image_records: BoardImageRecordStorageBase board_image_records: BoardImageRecordStorageBase
metadata: MetadataServiceBase
urls: UrlServiceBase urls: UrlServiceBase
logger: Logger logger: Logger
names: NameServiceBase names: NameServiceBase
@ -135,7 +131,6 @@ class ImageServiceDependencies:
image_record_storage: ImageRecordStorageBase, image_record_storage: ImageRecordStorageBase,
image_file_storage: ImageFileStorageBase, image_file_storage: ImageFileStorageBase,
board_image_record_storage: BoardImageRecordStorageBase, board_image_record_storage: BoardImageRecordStorageBase,
metadata: MetadataServiceBase,
url: UrlServiceBase, url: UrlServiceBase,
logger: Logger, logger: Logger,
names: NameServiceBase, names: NameServiceBase,
@ -144,7 +139,6 @@ class ImageServiceDependencies:
self.image_records = image_record_storage self.image_records = image_record_storage
self.image_files = image_file_storage self.image_files = image_file_storage
self.board_image_records = board_image_record_storage self.board_image_records = board_image_record_storage
self.metadata = metadata
self.urls = url self.urls = url
self.logger = logger self.logger = logger
self.names = names self.names = names
@ -165,6 +159,7 @@ class ImageService(ImageServiceABC):
node_id: Optional[str] = None, node_id: Optional[str] = None,
session_id: Optional[str] = None, session_id: Optional[str] = None,
is_intermediate: bool = False, is_intermediate: bool = False,
metadata: Optional[dict] = None,
) -> ImageDTO: ) -> ImageDTO:
if image_origin not in ResourceOrigin: if image_origin not in ResourceOrigin:
raise InvalidOriginException raise InvalidOriginException
@ -174,7 +169,16 @@ class ImageService(ImageServiceABC):
image_name = self._services.names.create_image_name() image_name = self._services.names.create_image_name()
metadata = self._get_metadata(session_id, node_id) graph = None
if session_id is not None:
session_raw = self._services.graph_execution_manager.get_raw(session_id)
if session_raw is not None:
try:
graph = get_metadata_graph_from_raw_session(session_raw)
except Exception as e:
self._services.logger.warn(f"Failed to parse session graph: {e}")
graph = None
(width, height) = image.size (width, height) = image.size
@ -191,14 +195,12 @@ class ImageService(ImageServiceABC):
is_intermediate=is_intermediate, is_intermediate=is_intermediate,
# Nullable fields # Nullable fields
node_id=node_id, node_id=node_id,
session_id=session_id,
metadata=metadata, metadata=metadata,
session_id=session_id,
) )
self._services.image_files.save( self._services.image_files.save(
image_name=image_name, image_name=image_name, image=image, metadata=metadata, graph=graph
image=image,
metadata=metadata,
) )
image_dto = self.get_dto(image_name) image_dto = self.get_dto(image_name)
@ -268,6 +270,34 @@ class ImageService(ImageServiceABC):
self._services.logger.error("Problem getting image DTO") self._services.logger.error("Problem getting image DTO")
raise e raise e
def get_metadata(self, image_name: str) -> Optional[ImageMetadata]:
try:
image_record = self._services.image_records.get(image_name)
if not image_record.session_id:
return ImageMetadata()
session_raw = self._services.graph_execution_manager.get_raw(
image_record.session_id
)
graph = None
if session_raw:
try:
graph = get_metadata_graph_from_raw_session(session_raw)
except Exception as e:
self._services.logger.warn(f"Failed to parse session graph: {e}")
graph = None
metadata = self._services.image_records.get_metadata(image_name)
return ImageMetadata(graph=graph, metadata=metadata)
except ImageRecordNotFoundException:
self._services.logger.error("Image record not found")
raise
except Exception as e:
self._services.logger.error("Problem getting image DTO")
raise e
def get_path(self, image_name: str, thumbnail: bool = False) -> str: def get_path(self, image_name: str, thumbnail: bool = False) -> str:
try: try:
return self._services.image_files.get_path(image_name, thumbnail) return self._services.image_files.get_path(image_name, thumbnail)
@ -367,15 +397,3 @@ class ImageService(ImageServiceABC):
except Exception as e: except Exception as e:
self._services.logger.error("Problem deleting image records and files") self._services.logger.error("Problem deleting image records and files")
raise e raise e
def _get_metadata(
self, session_id: Optional[str] = None, node_id: Optional[str] = None
) -> Optional[ImageMetadata]:
"""Get the metadata for a node."""
metadata = None
if node_id is not None and session_id is not None:
session = self._services.graph_execution_manager.get(session_id)
metadata = self._services.metadata.create_image_metadata(session, node_id)
return metadata

View File

@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Callable, Generic, TypeVar from typing import Callable, Generic, Optional, TypeVar
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pydantic.generics import GenericModel from pydantic.generics import GenericModel
@ -29,14 +29,22 @@ class ItemStorageABC(ABC, Generic[T]):
@abstractmethod @abstractmethod
def get(self, item_id: str) -> T: def get(self, item_id: str) -> T:
"""Gets the item, parsing it into a Pydantic model"""
pass
@abstractmethod
def get_raw(self, item_id: str) -> Optional[str]:
"""Gets the raw item as a string, skipping Pydantic parsing"""
pass pass
@abstractmethod @abstractmethod
def set(self, item: T) -> None: def set(self, item: T) -> None:
"""Sets the item"""
pass pass
@abstractmethod @abstractmethod
def list(self, page: int = 0, per_page: int = 10) -> PaginatedResults[T]: def list(self, page: int = 0, per_page: int = 10) -> PaginatedResults[T]:
"""Gets a paginated list of items"""
pass pass
@abstractmethod @abstractmethod

View File

@ -1,142 +0,0 @@
from abc import ABC, abstractmethod
from typing import Any, Optional
import networkx as nx
from invokeai.app.models.metadata import ImageMetadata
from invokeai.app.services.graph import Graph, GraphExecutionState
class MetadataServiceBase(ABC):
"""Handles building metadata for nodes, images, and outputs."""
@abstractmethod
def create_image_metadata(
self, session: GraphExecutionState, node_id: str
) -> ImageMetadata:
"""Builds an ImageMetadata object for a node."""
pass
class CoreMetadataService(MetadataServiceBase):
_ANCESTOR_TYPES = ["t2l", "l2l"]
"""The ancestor types that contain the core metadata"""
_ANCESTOR_PARAMS = ["type", "steps", "model", "cfg_scale", "scheduler", "strength"]
"""The core metadata parameters in the ancestor types"""
_NOISE_FIELDS = ["seed", "width", "height"]
"""The core metadata parameters in the noise node"""
def create_image_metadata(
self, session: GraphExecutionState, node_id: str
) -> ImageMetadata:
metadata = self._build_metadata_from_graph(session, node_id)
return metadata
def _find_nearest_ancestor(self, G: nx.DiGraph, node_id: str) -> Optional[str]:
"""
Finds the id of the nearest ancestor (of a valid type) of a given node.
Parameters:
G (nx.DiGraph): The execution graph, converted in to a networkx DiGraph. Its nodes must
have the same data as the execution graph.
node_id (str): The ID of the node.
Returns:
str | None: The ID of the nearest ancestor, or None if there are no valid ancestors.
"""
# Retrieve the node from the graph
node = G.nodes[node_id]
# If the node type is one of the core metadata node types, return its id
if node.get("type") in self._ANCESTOR_TYPES:
return node.get("id")
# Else, look for the ancestor in the predecessor nodes
for predecessor in G.predecessors(node_id):
result = self._find_nearest_ancestor(G, predecessor)
if result:
return result
# If there are no valid ancestors, return None
return None
def _get_additional_metadata(
self, graph: Graph, node_id: str
) -> Optional[dict[str, Any]]:
"""
Returns additional metadata for a given node.
Parameters:
graph (Graph): The execution graph.
node_id (str): The ID of the node.
Returns:
dict[str, Any] | None: A dictionary of additional metadata.
"""
metadata = {}
# Iterate over all edges in the graph
for edge in graph.edges:
dest_node_id = edge.destination.node_id
dest_field = edge.destination.field
source_node_dict = graph.nodes[edge.source.node_id].dict()
# If the destination node ID matches the given node ID, gather necessary metadata
if dest_node_id == node_id:
# Prompt
if dest_field == "positive_conditioning":
metadata["positive_conditioning"] = source_node_dict.get("prompt")
# Negative prompt
if dest_field == "negative_conditioning":
metadata["negative_conditioning"] = source_node_dict.get("prompt")
# Seed, width and height
if dest_field == "noise":
for field in self._NOISE_FIELDS:
metadata[field] = source_node_dict.get(field)
return metadata
def _build_metadata_from_graph(
self, session: GraphExecutionState, node_id: str
) -> ImageMetadata:
"""
Builds an ImageMetadata object for a node.
Parameters:
session (GraphExecutionState): The session.
node_id (str): The ID of the node.
Returns:
ImageMetadata: The metadata for the node.
"""
# We need to do all the traversal on the execution graph
graph = session.execution_graph
# Find the nearest `t2l`/`l2l` ancestor of the given node
ancestor_id = self._find_nearest_ancestor(graph.nx_graph_with_data(), node_id)
# If no ancestor was found, return an empty ImageMetadata object
if ancestor_id is None:
return ImageMetadata()
ancestor_node = graph.get_node(ancestor_id)
# Grab all the core metadata from the ancestor node
ancestor_metadata = {
param: val
for param, val in ancestor_node.dict().items()
if param in self._ANCESTOR_PARAMS
}
# Get this image's prompts and noise parameters
addl_metadata = self._get_additional_metadata(graph, ancestor_id)
# If additional metadata was found, add it to the main metadata
if addl_metadata is not None:
ancestor_metadata.update(addl_metadata)
return ImageMetadata(**ancestor_metadata)

View File

@ -1,13 +1,14 @@
import datetime import datetime
from typing import Optional, Union from typing import Optional, Union
from pydantic import BaseModel, Extra, Field, StrictBool, StrictStr from pydantic import BaseModel, Extra, Field, StrictBool, StrictStr
from invokeai.app.models.image import ImageCategory, ResourceOrigin from invokeai.app.models.image import ImageCategory, ResourceOrigin
from invokeai.app.models.metadata import ImageMetadata
from invokeai.app.util.misc import get_iso_timestamp from invokeai.app.util.misc import get_iso_timestamp
class ImageRecord(BaseModel): class ImageRecord(BaseModel):
"""Deserialized image record.""" """Deserialized image record without metadata."""
image_name: str = Field(description="The unique name of the image.") image_name: str = Field(description="The unique name of the image.")
"""The unique name of the image.""" """The unique name of the image."""
@ -43,11 +44,6 @@ class ImageRecord(BaseModel):
description="The node ID that generated this image, if it is a generated image.", description="The node ID that generated this image, if it is a generated image.",
) )
"""The node ID that generated this image, if it is a generated image.""" """The node ID that generated this image, if it is a generated image."""
metadata: Optional[ImageMetadata] = Field(
default=None,
description="A limited subset of the image's generation metadata. Retrieve the image's session for full metadata.",
)
"""A limited subset of the image's generation metadata. Retrieve the image's session for full metadata."""
class ImageRecordChanges(BaseModel, extra=Extra.forbid): class ImageRecordChanges(BaseModel, extra=Extra.forbid):
@ -112,6 +108,7 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord:
# Retrieve all the values, setting "reasonable" defaults if they are not present. # Retrieve all the values, setting "reasonable" defaults if they are not present.
# TODO: do we really need to handle default values here? ideally the data is the correct shape...
image_name = image_dict.get("image_name", "unknown") image_name = image_dict.get("image_name", "unknown")
image_origin = ResourceOrigin( image_origin = ResourceOrigin(
image_dict.get("image_origin", ResourceOrigin.INTERNAL.value) image_dict.get("image_origin", ResourceOrigin.INTERNAL.value)
@ -128,13 +125,6 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord:
deleted_at = image_dict.get("deleted_at", get_iso_timestamp()) deleted_at = image_dict.get("deleted_at", get_iso_timestamp())
is_intermediate = image_dict.get("is_intermediate", False) is_intermediate = image_dict.get("is_intermediate", False)
raw_metadata = image_dict.get("metadata")
if raw_metadata is not None:
metadata = ImageMetadata.parse_raw(raw_metadata)
else:
metadata = None
return ImageRecord( return ImageRecord(
image_name=image_name, image_name=image_name,
image_origin=image_origin, image_origin=image_origin,
@ -143,7 +133,6 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord:
height=height, height=height,
session_id=session_id, session_id=session_id,
node_id=node_id, node_id=node_id,
metadata=metadata,
created_at=created_at, created_at=created_at,
updated_at=updated_at, updated_at=updated_at,
deleted_at=deleted_at, deleted_at=deleted_at,

View File

@ -1,6 +1,6 @@
import sqlite3 import sqlite3
from threading import Lock from threading import Lock
from typing import Generic, TypeVar, Optional, Union, get_args from typing import Generic, Optional, TypeVar, get_args
from pydantic import BaseModel, parse_raw_as from pydantic import BaseModel, parse_raw_as
@ -78,6 +78,21 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]):
return self._parse_item(result[0]) return self._parse_item(result[0])
def get_raw(self, id: str) -> Optional[str]:
try:
self._lock.acquire()
self._cursor.execute(
f"""SELECT item FROM {self._table_name} WHERE id = ?;""", (str(id),)
)
result = self._cursor.fetchone()
finally:
self._lock.release()
if not result:
return None
return result[0]
def delete(self, id: str): def delete(self, id: str):
try: try:
self._lock.acquire() self._lock.acquire()

View File

@ -22,4 +22,4 @@ class LocalUrlService(UrlServiceBase):
if thumbnail: if thumbnail:
return f"{self._base_url}/images/{image_basename}/thumbnail" return f"{self._base_url}/images/{image_basename}/thumbnail"
return f"{self._base_url}/images/{image_basename}" return f"{self._base_url}/images/{image_basename}/full"

View File

@ -0,0 +1,55 @@
import json
from typing import Optional
from pydantic import ValidationError
from invokeai.app.services.graph import Edge
def get_metadata_graph_from_raw_session(session_raw: str) -> Optional[dict]:
"""
Parses raw session string, returning a dict of the graph.
Only the general graph shape is validated; none of the fields are validated.
Any `metadata_accumulator` nodes and edges are removed.
Any validation failure will return None.
"""
graph = json.loads(session_raw).get("graph", None)
# sanity check make sure the graph is at least reasonably shaped
if (
type(graph) is not dict
or "nodes" not in graph
or type(graph["nodes"]) is not dict
or "edges" not in graph
or type(graph["edges"]) is not list
):
# something has gone terribly awry, return an empty dict
return None
try:
# delete the `metadata_accumulator` node
del graph["nodes"]["metadata_accumulator"]
except KeyError:
# no accumulator node, all good
pass
# delete any edges to or from it
for i, edge in enumerate(graph["edges"]):
try:
# try to parse the edge
Edge(**edge)
except ValidationError:
# something has gone terribly awry, return an empty dict
return None
if (
edge["source"]["node_id"] == "metadata_accumulator"
or edge["destination"]["node_id"] == "metadata_accumulator"
):
del graph["edges"][i]
return graph

View File

@ -121,8 +121,8 @@ class ModelInstall(object):
installed_models = self.mgr.list_models() installed_models = self.mgr.list_models()
for md in installed_models: for md in installed_models:
base = md['base_model'] base = md['base_model']
model_type = md['type'] model_type = md['model_type']
name = md['name'] name = md['model_name']
key = ModelManager.create_key(name, base, model_type) key = ModelManager.create_key(name, base, model_type)
if key in model_dict: if key in model_dict:
model_dict[key].installed = True model_dict[key].installed = True

View File

@ -538,9 +538,9 @@ class ModelManager(object):
model_dict = dict( model_dict = dict(
**model_config.dict(exclude_defaults=True), **model_config.dict(exclude_defaults=True),
# OpenAPIModelInfoBase # OpenAPIModelInfoBase
name=cur_model_name, model_name=cur_model_name,
base_model=cur_base_model, base_model=cur_base_model,
type=cur_model_type, model_type=cur_model_type,
) )
models.append(model_dict) models.append(model_dict)

View File

@ -37,9 +37,9 @@ MODEL_CONFIGS = list()
OPENAPI_MODEL_CONFIGS = list() OPENAPI_MODEL_CONFIGS = list()
class OpenAPIModelInfoBase(BaseModel): class OpenAPIModelInfoBase(BaseModel):
name: str model_name: str
base_model: BaseModelType base_model: BaseModelType
type: ModelType model_type: ModelType
for base_model, models in MODEL_CLASSES.items(): for base_model, models in MODEL_CLASSES.items():
@ -56,7 +56,7 @@ for base_model, models in MODEL_CLASSES.items():
api_wrapper = type(openapi_cfg_name, (cfg, OpenAPIModelInfoBase), dict( api_wrapper = type(openapi_cfg_name, (cfg, OpenAPIModelInfoBase), dict(
__annotations__ = dict( __annotations__ = dict(
type=Literal[model_type.value], model_type=Literal[model_type.value],
), ),
)) ))

View File

@ -108,6 +108,7 @@
"roarr": "^7.15.0", "roarr": "^7.15.0",
"serialize-error": "^11.0.0", "serialize-error": "^11.0.0",
"socket.io-client": "^4.7.0", "socket.io-client": "^4.7.0",
"use-debounce": "^9.0.4",
"use-image": "^1.1.1", "use-image": "^1.1.1",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"zod": "^3.21.4" "zod": "^3.21.4"

View File

@ -102,7 +102,8 @@
"openInNewTab": "Open in New Tab", "openInNewTab": "Open in New Tab",
"dontAskMeAgain": "Don't ask me again", "dontAskMeAgain": "Don't ask me again",
"areYouSure": "Are you sure?", "areYouSure": "Are you sure?",
"imagePrompt": "Image Prompt" "imagePrompt": "Image Prompt",
"clearNodes": "Are you sure you want to clear all nodes?"
}, },
"gallery": { "gallery": {
"generations": "Generations", "generations": "Generations",
@ -596,7 +597,9 @@
"initialImageNotSetDesc": "Could not load initial image", "initialImageNotSetDesc": "Could not load initial image",
"nodesSaved": "Nodes Saved", "nodesSaved": "Nodes Saved",
"nodesLoaded": "Nodes Loaded", "nodesLoaded": "Nodes Loaded",
"nodesLoadedFailed": "Failed To Load Nodes" "nodesLoadedFailed": "Failed To Load Nodes",
"nodesCleared": "Nodes Cleared"
}, },
"tooltip": { "tooltip": {
"feature": { "feature": {
@ -681,6 +684,7 @@
"nodes": { "nodes": {
"reloadSchema": "Reload Schema", "reloadSchema": "Reload Schema",
"saveNodes": "Save Nodes", "saveNodes": "Save Nodes",
"loadNodes": "Load Nodes" "loadNodes": "Load Nodes",
"clearNodes": "Clear Nodes"
} }
} }

View File

@ -51,6 +51,7 @@ import {
} from './listeners/imageUrlsReceived'; } from './listeners/imageUrlsReceived';
import { addInitialImageSelectedListener } from './listeners/initialImageSelected'; import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
import { addModelSelectedListener } from './listeners/modelSelected'; import { addModelSelectedListener } from './listeners/modelSelected';
import { addModelsLoadedListener } from './listeners/modelsLoaded';
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema'; import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
import { import {
addReceivedPageOfImagesFulfilledListener, addReceivedPageOfImagesFulfilledListener,
@ -224,3 +225,4 @@ addModelSelectedListener();
// app startup // app startup
addAppStartedListener(); addAppStartedListener();
addModelsLoadedListener();

View File

@ -1,13 +1,13 @@
import { startAppListening } from '..';
import { imageMetadataReceived } from 'services/api/thunks/image';
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { controlNetImageProcessed } from 'features/controlNet/store/actions'; import { controlNetImageProcessed } from 'features/controlNet/store/actions';
import { Graph } from 'services/api/types';
import { sessionCreated } from 'services/api/thunks/session';
import { sessionReadyToInvoke } from 'features/system/store/actions';
import { socketInvocationComplete } from 'services/events/actions';
import { isImageOutput } from 'services/api/guards';
import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice'; import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice';
import { sessionReadyToInvoke } from 'features/system/store/actions';
import { isImageOutput } from 'services/api/guards';
import { imageDTOReceived } from 'services/api/thunks/image';
import { sessionCreated } from 'services/api/thunks/session';
import { Graph } from 'services/api/types';
import { socketInvocationComplete } from 'services/events/actions';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'controlNet' }); const moduleLog = log.child({ namespace: 'controlNet' });
@ -63,10 +63,8 @@ export const addControlNetImageProcessedListener = () => {
// Wait for the ImageDTO to be received // Wait for the ImageDTO to be received
const [imageMetadataReceivedAction] = await take( const [imageMetadataReceivedAction] = await take(
( (action): action is ReturnType<typeof imageDTOReceived.fulfilled> =>
action imageDTOReceived.fulfilled.match(action) &&
): action is ReturnType<typeof imageMetadataReceived.fulfilled> =>
imageMetadataReceived.fulfilled.match(action) &&
action.payload.image_name === image_name action.payload.image_name === image_name
); );
const processedControlImage = imageMetadataReceivedAction.payload; const processedControlImage = imageMetadataReceivedAction.payload;

View File

@ -1,7 +1,7 @@
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageMetadataReceived } from 'services/api/thunks/image';
import { boardImagesApi } from 'services/api/endpoints/boardImages'; import { boardImagesApi } from 'services/api/endpoints/boardImages';
import { imageDTOReceived } from 'services/api/thunks/image';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'boards' }); const moduleLog = log.child({ namespace: 'boards' });
@ -17,7 +17,7 @@ export const addImageAddedToBoardFulfilledListener = () => {
); );
dispatch( dispatch(
imageMetadataReceived({ imageDTOReceived({
image_name, image_name,
}) })
); );

View File

@ -1,13 +1,13 @@
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageMetadataReceived, imageUpdated } from 'services/api/thunks/image';
import { imageUpserted } from 'features/gallery/store/gallerySlice'; import { imageUpserted } from 'features/gallery/store/gallerySlice';
import { imageDTOReceived, imageUpdated } from 'services/api/thunks/image';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'image' }); const moduleLog = log.child({ namespace: 'image' });
export const addImageMetadataReceivedFulfilledListener = () => { export const addImageMetadataReceivedFulfilledListener = () => {
startAppListening({ startAppListening({
actionCreator: imageMetadataReceived.fulfilled, actionCreator: imageDTOReceived.fulfilled,
effect: (action, { getState, dispatch }) => { effect: (action, { getState, dispatch }) => {
const image = action.payload; const image = action.payload;
@ -40,7 +40,7 @@ export const addImageMetadataReceivedFulfilledListener = () => {
export const addImageMetadataReceivedRejectedListener = () => { export const addImageMetadataReceivedRejectedListener = () => {
startAppListening({ startAppListening({
actionCreator: imageMetadataReceived.rejected, actionCreator: imageDTOReceived.rejected,
effect: (action, { getState, dispatch }) => { effect: (action, { getState, dispatch }) => {
moduleLog.debug( moduleLog.debug(
{ data: { image: action.meta.arg } }, { data: { image: action.meta.arg } },

View File

@ -1,7 +1,7 @@
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageMetadataReceived } from 'services/api/thunks/image';
import { boardImagesApi } from 'services/api/endpoints/boardImages'; import { boardImagesApi } from 'services/api/endpoints/boardImages';
import { imageDTOReceived } from 'services/api/thunks/image';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'boards' }); const moduleLog = log.child({ namespace: 'boards' });
@ -17,7 +17,7 @@ export const addImageRemovedFromBoardFulfilledListener = () => {
); );
dispatch( dispatch(
imageMetadataReceived({ imageDTOReceived({
image_name, image_name,
}) })
); );

View File

@ -14,7 +14,7 @@ export const addModelSelectedListener = () => {
actionCreator: modelSelected, actionCreator: modelSelected,
effect: (action, { getState, dispatch }) => { effect: (action, { getState, dispatch }) => {
const state = getState(); const state = getState();
const [base_model, type, name] = action.payload.split('/'); const { base_model, model_name } = action.payload;
if (state.generation.model?.base_model !== base_model) { if (state.generation.model?.base_model !== base_model) {
dispatch( dispatch(
@ -30,11 +30,7 @@ export const addModelSelectedListener = () => {
// TODO: controlnet cleared // TODO: controlnet cleared
} }
const newModel = zMainModel.parse({ const newModel = zMainModel.parse(action.payload);
id: action.payload,
base_model,
name,
});
dispatch(modelChanged(newModel)); dispatch(modelChanged(newModel));
}, },

View File

@ -0,0 +1,42 @@
import { modelChanged } from 'features/parameters/store/generationSlice';
import { some } from 'lodash-es';
import { modelsApi } from 'services/api/endpoints/models';
import { startAppListening } from '..';
export const addModelsLoadedListener = () => {
startAppListening({
matcher: modelsApi.endpoints.getMainModels.matchFulfilled,
effect: async (action, { getState, dispatch }) => {
// models loaded, we need to ensure the selected model is available and if not, select the first one
const currentModel = getState().generation.model;
const isCurrentModelAvailable = some(
action.payload.entities,
(m) =>
m?.model_name === currentModel?.model_name &&
m?.base_model === currentModel?.base_model
);
if (isCurrentModelAvailable) {
return;
}
const firstModelId = action.payload.ids[0];
const firstModel = action.payload.entities[firstModelId];
if (!firstModel) {
// No models loaded at all
dispatch(modelChanged(null));
return;
}
dispatch(
modelChanged({
base_model: firstModel.base_model,
model_name: firstModel.model_name,
})
);
},
});
};

View File

@ -30,6 +30,7 @@ export const addSessionCreatedRejectedListener = () => {
effect: (action, { getState, dispatch }) => { effect: (action, { getState, dispatch }) => {
if (action.payload) { if (action.payload) {
const { arg, error } = action.payload; const { arg, error } = action.payload;
const stringifiedError = JSON.stringify(error);
moduleLog.error( moduleLog.error(
{ {
data: { data: {
@ -37,7 +38,7 @@ export const addSessionCreatedRejectedListener = () => {
error: serializeError(error), error: serializeError(error),
}, },
}, },
`Problem creating session` `Problem creating session: ${stringifiedError}`
); );
} }
}, },

View File

@ -33,6 +33,7 @@ export const addSessionInvokedRejectedListener = () => {
effect: (action, { getState, dispatch }) => { effect: (action, { getState, dispatch }) => {
if (action.payload) { if (action.payload) {
const { arg, error } = action.payload; const { arg, error } = action.payload;
const stringifiedError = JSON.stringify(error);
moduleLog.error( moduleLog.error(
{ {
data: { data: {
@ -40,7 +41,7 @@ export const addSessionInvokedRejectedListener = () => {
error: serializeError(error), error: serializeError(error),
}, },
}, },
`Problem invoking session` `Problem invoking session: ${stringifiedError}`
); );
} }
}, },

View File

@ -1,15 +1,15 @@
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import { startAppListening } from '../..';
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import { progressImageSet } from 'features/system/store/systemSlice';
import { boardImagesApi } from 'services/api/endpoints/boardImages';
import { isImageOutput } from 'services/api/guards';
import { imageDTOReceived } from 'services/api/thunks/image';
import { sessionCanceled } from 'services/api/thunks/session';
import { import {
appSocketInvocationComplete, appSocketInvocationComplete,
socketInvocationComplete, socketInvocationComplete,
} from 'services/events/actions'; } from 'services/events/actions';
import { imageMetadataReceived } from 'services/api/thunks/image'; import { startAppListening } from '../..';
import { sessionCanceled } from 'services/api/thunks/session';
import { isImageOutput } from 'services/api/guards';
import { progressImageSet } from 'features/system/store/systemSlice';
import { boardImagesApi } from 'services/api/endpoints/boardImages';
const moduleLog = log.child({ namespace: 'socketio' }); const moduleLog = log.child({ namespace: 'socketio' });
const nodeDenylist = ['dataURL_image']; const nodeDenylist = ['dataURL_image'];
@ -42,13 +42,13 @@ export const addInvocationCompleteEventListener = () => {
// Get its metadata // Get its metadata
dispatch( dispatch(
imageMetadataReceived({ imageDTOReceived({
image_name, image_name,
}) })
); );
const [{ payload: imageDTO }] = await take( const [{ payload: imageDTO }] = await take(
imageMetadataReceived.fulfilled.match imageDTOReceived.fulfilled.match
); );
// Handle canvas image // Handle canvas image

View File

@ -13,7 +13,7 @@ export const addInvocationErrorEventListener = () => {
effect: (action, { dispatch, getState }) => { effect: (action, { dispatch, getState }) => {
moduleLog.error( moduleLog.error(
action.payload, action.payload,
`Invocation error (${action.payload.data.node.type})` `Invocation error (${action.payload.data.node.type}): ${action.payload.data.error}`
); );
dispatch(appSocketInvocationError(action.payload)); dispatch(appSocketInvocationError(action.payload));
}, },

View File

@ -1,6 +1,7 @@
import { import {
AnyAction, AnyAction,
ThunkDispatch, ThunkDispatch,
autoBatchEnhancer,
combineReducers, combineReducers,
configureStore, configureStore,
} from '@reduxjs/toolkit'; } from '@reduxjs/toolkit';
@ -79,14 +80,18 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
export const store = configureStore({ export const store = configureStore({
reducer: rememberedRootReducer, reducer: rememberedRootReducer,
enhancers: [ enhancers: (existingEnhancers) => {
rememberEnhancer(window.localStorage, rememberedKeys, { return existingEnhancers
persistDebounce: 300, .concat(
serialize, rememberEnhancer(window.localStorage, rememberedKeys, {
unserialize, persistDebounce: 300,
prefix: LOCALSTORAGE_PREFIX, serialize,
}), unserialize,
], prefix: LOCALSTORAGE_PREFIX,
})
)
.concat(autoBatchEnhancer());
},
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ getDefaultMiddleware({
immutableCheck: false, immutableCheck: false,

View File

@ -47,8 +47,8 @@ const ParamEmbeddingPopover = (props: Props) => {
const disabled = currentMainModel?.base_model !== embedding.base_model; const disabled = currentMainModel?.base_model !== embedding.base_model;
data.push({ data.push({
value: embedding.name, value: embedding.model_name,
label: embedding.name, label: embedding.model_name,
group: MODEL_TYPE_MAP[embedding.base_model], group: MODEL_TYPE_MAP[embedding.base_model],
disabled, disabled,
tooltip: disabled tooltip: disabled

View File

@ -45,7 +45,11 @@ import {
FaShare, FaShare,
FaShareAlt, FaShareAlt,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import {
useGetImageDTOQuery,
useGetImageMetadataQuery,
} from 'services/api/endpoints/images';
import { useDebounce } from 'use-debounce';
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions'; import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
const currentImageButtonsSelector = createSelector( const currentImageButtonsSelector = createSelector(
@ -128,10 +132,23 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const { recallBothPrompts, recallSeed, recallAllParameters } = const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters(); useRecallParameters();
const { currentData: image } = useGetImageDTOQuery( const [debouncedMetadataQueryArg, debounceState] = useDebounce(
lastSelectedImage,
500
);
const { currentData: image, isFetching } = useGetImageDTOQuery(
lastSelectedImage ?? skipToken lastSelectedImage ?? skipToken
); );
const { currentData: metadataData } = useGetImageMetadataQuery(
debounceState.isPending()
? skipToken
: debouncedMetadataQueryArg ?? skipToken
);
const metadata = metadataData?.metadata;
// const handleCopyImage = useCallback(async () => { // const handleCopyImage = useCallback(async () => {
// if (!image?.url) { // if (!image?.url) {
// return; // return;
@ -193,29 +210,26 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}, [toaster, t, image]); }, [toaster, t, image]);
const handleClickUseAllParameters = useCallback(() => { const handleClickUseAllParameters = useCallback(() => {
recallAllParameters(image); recallAllParameters(metadata);
}, [image, recallAllParameters]); }, [metadata, recallAllParameters]);
useHotkeys( useHotkeys(
'a', 'a',
() => { () => {
handleClickUseAllParameters; handleClickUseAllParameters;
}, },
[image, recallAllParameters] [metadata, recallAllParameters]
); );
const handleUseSeed = useCallback(() => { const handleUseSeed = useCallback(() => {
recallSeed(image?.metadata?.seed); recallSeed(metadata?.seed);
}, [image, recallSeed]); }, [metadata?.seed, recallSeed]);
useHotkeys('s', handleUseSeed, [image]); useHotkeys('s', handleUseSeed, [image]);
const handleUsePrompt = useCallback(() => { const handleUsePrompt = useCallback(() => {
recallBothPrompts( recallBothPrompts(metadata?.positive_prompt, metadata?.negative_prompt);
image?.metadata?.positive_conditioning, }, [metadata?.negative_prompt, metadata?.positive_prompt, recallBothPrompts]);
image?.metadata?.negative_conditioning
);
}, [image, recallBothPrompts]);
useHotkeys('p', handleUsePrompt, [image]); useHotkeys('p', handleUsePrompt, [image]);
@ -440,7 +454,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
icon={<FaQuoteRight />} icon={<FaQuoteRight />}
tooltip={`${t('parameters.usePrompt')} (P)`} tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`} aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!image?.metadata?.positive_conditioning} isDisabled={!metadata?.positive_prompt}
onClick={handleUsePrompt} onClick={handleUsePrompt}
/> />
@ -448,7 +462,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
icon={<FaSeedling />} icon={<FaSeedling />}
tooltip={`${t('parameters.useSeed')} (S)`} tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`} aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!image?.metadata?.seed} isDisabled={!metadata?.seed}
onClick={handleUseSeed} onClick={handleUseSeed}
/> />
@ -456,10 +470,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
icon={<FaAsterisk />} icon={<FaAsterisk />}
tooltip={`${t('parameters.useAll')} (A)`} tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`} aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={ isDisabled={!metadata}
// not sure what this list should be
!['t2l', 'l2l', 'inpaint'].includes(String(image?.metadata?.type))
}
onClick={handleClickUseAllParameters} onClick={handleClickUseAllParameters}
/> />
</ButtonGroup> </ButtonGroup>

View File

@ -11,7 +11,9 @@ import IAIDndImage from 'common/components/IAIDndImage';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySlice'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySlice';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useNextPrevImage } from '../hooks/useNextPrevImage';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons'; import NextPrevImageButtons from './NextPrevImageButtons';
@ -49,6 +51,45 @@ const CurrentImagePreview = () => {
shouldAntialiasProgressImage, shouldAntialiasProgressImage,
} = useAppSelector(imagesSelector); } = useAppSelector(imagesSelector);
const {
handlePrevImage,
handleNextImage,
prevImageId,
nextImageId,
isOnLastImage,
handleLoadMoreImages,
areMoreImagesAvailable,
isFetching,
} = useNextPrevImage();
useHotkeys(
'left',
() => {
handlePrevImage();
},
[prevImageId]
);
useHotkeys(
'right',
() => {
if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
handleLoadMoreImages();
return;
}
if (!isOnLastImage) {
handleNextImage();
}
},
[
nextImageId,
isOnLastImage,
areMoreImagesAvailable,
handleLoadMoreImages,
isFetching,
]
);
const { const {
currentData: imageDTO, currentData: imageDTO,
isLoading, isLoading,
@ -118,7 +159,6 @@ const CurrentImagePreview = () => {
width: 'full', width: 'full',
height: 'full', height: 'full',
borderRadius: 'base', borderRadius: 'base',
overflow: 'scroll',
}} }}
> >
<ImageMetadataViewer image={imageDTO} /> <ImageMetadataViewer image={imageDTO} />

View File

@ -6,10 +6,7 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
import { import { imagesAddedToBatch } from 'features/batch/store/batchSlice';
imagesAddedToBatch,
selectionAddedToBatch,
} from 'features/batch/store/batchSlice';
import { import {
resizeAndScaleCanvas, resizeAndScaleCanvas,
setInitialCanvasImage, setInitialCanvasImage,
@ -24,6 +21,7 @@ import { useTranslation } from 'react-i18next';
import { FaExpand, FaFolder, FaShare, FaTrash } from 'react-icons/fa'; import { FaExpand, FaFolder, FaShare, FaTrash } from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages'; import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
import { ImageDTO } from 'services/api/types'; import { ImageDTO } from 'services/api/types';
import { AddImageToBoardContext } from '../../../app/contexts/AddImageToBoardContext'; import { AddImageToBoardContext } from '../../../app/contexts/AddImageToBoardContext';
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions'; import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
@ -38,24 +36,17 @@ const ImageContextMenu = ({ image, children }: Props) => {
() => () =>
createSelector( createSelector(
[stateSelector], [stateSelector],
({ gallery, batch }) => { ({ gallery }) => {
const selectionCount = gallery.selection.length; const selectionCount = gallery.selection.length;
const isInBatch = batch.imageNames.includes(image.image_name);
return { selectionCount, isInBatch }; return { selectionCount };
}, },
defaultSelectorOptions defaultSelectorOptions
), ),
[image.image_name] []
); );
const { selectionCount, isInBatch } = useAppSelector(selector); const { selectionCount } = useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const toaster = useAppToaster();
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const { onClickAddToBoard } = useContext(AddImageToBoardContext); const { onClickAddToBoard } = useContext(AddImageToBoardContext);
@ -66,178 +57,17 @@ const ImageContextMenu = ({ image, children }: Props) => {
dispatch(imageToDeleteSelected(image)); dispatch(imageToDeleteSelected(image));
}, [dispatch, image]); }, [dispatch, image]);
const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters();
const [removeFromBoard] = useRemoveImageFromBoardMutation();
// Recall parameters handlers
const handleRecallPrompt = useCallback(() => {
recallBothPrompts(
image.metadata?.positive_conditioning,
image.metadata?.negative_conditioning
);
}, [
image.metadata?.negative_conditioning,
image.metadata?.positive_conditioning,
recallBothPrompts,
]);
const handleRecallSeed = useCallback(() => {
recallSeed(image.metadata?.seed);
}, [image, recallSeed]);
const handleSendToImageToImage = useCallback(() => {
dispatch(sentImageToImg2Img());
dispatch(initialImageSelected(image));
}, [dispatch, image]);
// const handleRecallInitialImage = useCallback(() => {
// recallInitialImage(image.metadata.invokeai?.node?.image);
// }, [image, recallInitialImage]);
const handleSendToCanvas = () => {
dispatch(sentImageToCanvas());
dispatch(setInitialCanvasImage(image));
dispatch(resizeAndScaleCanvas());
dispatch(setActiveTab('unifiedCanvas'));
toaster({
title: t('toast.sentToUnifiedCanvas'),
status: 'success',
duration: 2500,
isClosable: true,
});
};
const handleUseAllParameters = useCallback(() => {
recallAllParameters(image);
}, [image, recallAllParameters]);
const handleLightBox = () => {
// dispatch(setCurrentImage(image));
// dispatch(setIsLightboxOpen(true));
};
const handleAddToBoard = useCallback(() => { const handleAddToBoard = useCallback(() => {
onClickAddToBoard(image); onClickAddToBoard(image);
}, [image, onClickAddToBoard]); }, [image, onClickAddToBoard]);
const handleRemoveFromBoard = useCallback(() => {
if (!image.board_id) {
return;
}
removeFromBoard({ board_id: image.board_id, image_name: image.image_name });
}, [image.board_id, image.image_name, removeFromBoard]);
const handleOpenInNewTab = () => {
window.open(image.image_url, '_blank');
};
const handleAddSelectionToBatch = useCallback(() => {
dispatch(selectionAddedToBatch());
}, [dispatch]);
const handleAddToBatch = useCallback(() => {
dispatch(imagesAddedToBatch([image.image_name]));
}, [dispatch, image.image_name]);
return ( return (
<ContextMenu<HTMLDivElement> <ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }} menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => ( renderMenu={() => (
<MenuList sx={{ visibility: 'visible !important' }}> <MenuList sx={{ visibility: 'visible !important' }}>
{selectionCount === 1 ? ( {selectionCount === 1 ? (
<> <SingleSelectionMenuItems image={image} />
<MenuItem
icon={<ExternalLinkIcon />}
onClickCapture={handleOpenInNewTab}
>
{t('common.openInNewTab')}
</MenuItem>
{isLightboxEnabled && (
<MenuItem icon={<FaExpand />} onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</MenuItem>
)}
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleRecallPrompt}
isDisabled={
image?.metadata?.positive_conditioning === undefined
}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleRecallSeed}
isDisabled={image?.metadata?.seed === undefined}
>
{t('parameters.useSeed')}
</MenuItem>
{/* <MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleRecallInitialImage}
isDisabled={image?.metadata?.type !== 'img2img'}
>
{t('parameters.useInitImg')}
</MenuItem> */}
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseAllParameters}
isDisabled={
// what should these be
!['t2l', 'l2l', 'inpaint'].includes(
String(image?.metadata?.type)
)
}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={<FaShare />}
onClickCapture={handleSendToImageToImage}
id="send-to-img2img"
>
{t('parameters.sendToImg2Img')}
</MenuItem>
{isCanvasEnabled && (
<MenuItem
icon={<FaShare />}
onClickCapture={handleSendToCanvas}
id="send-to-canvas"
>
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
)}
{/* <MenuItem
icon={<FaFolder />}
isDisabled={isInBatch}
onClickCapture={handleAddToBatch}
>
Add to Batch
</MenuItem> */}
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
{image.board_id ? 'Change Board' : 'Add to Board'}
</MenuItem>
{image.board_id && (
<MenuItem
icon={<FaFolder />}
onClickCapture={handleRemoveFromBoard}
>
Remove from Board
</MenuItem>
)}
<MenuItem
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
icon={<FaTrash />}
onClickCapture={handleDelete}
>
{t('gallery.deleteImage')}
</MenuItem>
</>
) : ( ) : (
<> <>
<MenuItem <MenuItem
@ -271,3 +101,185 @@ const ImageContextMenu = ({ image, children }: Props) => {
}; };
export default memo(ImageContextMenu); export default memo(ImageContextMenu);
type SingleSelectionMenuItemsProps = {
image: ImageDTO;
};
const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const { image } = props;
const selector = useMemo(
() =>
createSelector(
[stateSelector],
({ batch }) => {
const isInBatch = batch.imageNames.includes(image.image_name);
return { isInBatch };
},
defaultSelectorOptions
),
[image.image_name]
);
const { isInBatch } = useAppSelector(selector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const toaster = useAppToaster();
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
const { currentData } = useGetImageMetadataQuery(image.image_name);
const metadata = currentData?.metadata;
const handleDelete = useCallback(() => {
if (!image) {
return;
}
dispatch(imageToDeleteSelected(image));
}, [dispatch, image]);
const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters();
const [removeFromBoard] = useRemoveImageFromBoardMutation();
// Recall parameters handlers
const handleRecallPrompt = useCallback(() => {
recallBothPrompts(metadata?.positive_prompt, metadata?.negative_prompt);
}, [metadata?.negative_prompt, metadata?.positive_prompt, recallBothPrompts]);
const handleRecallSeed = useCallback(() => {
recallSeed(metadata?.seed);
}, [metadata?.seed, recallSeed]);
const handleSendToImageToImage = useCallback(() => {
dispatch(sentImageToImg2Img());
dispatch(initialImageSelected(image));
}, [dispatch, image]);
const handleSendToCanvas = () => {
dispatch(sentImageToCanvas());
dispatch(setInitialCanvasImage(image));
dispatch(resizeAndScaleCanvas());
dispatch(setActiveTab('unifiedCanvas'));
toaster({
title: t('toast.sentToUnifiedCanvas'),
status: 'success',
duration: 2500,
isClosable: true,
});
};
const handleUseAllParameters = useCallback(() => {
console.log(metadata);
recallAllParameters(metadata);
}, [metadata, recallAllParameters]);
const handleLightBox = () => {
// dispatch(setCurrentImage(image));
// dispatch(setIsLightboxOpen(true));
};
const handleAddToBoard = useCallback(() => {
onClickAddToBoard(image);
}, [image, onClickAddToBoard]);
const handleRemoveFromBoard = useCallback(() => {
if (!image.board_id) {
return;
}
removeFromBoard({ board_id: image.board_id, image_name: image.image_name });
}, [image.board_id, image.image_name, removeFromBoard]);
const handleOpenInNewTab = () => {
window.open(image.image_url, '_blank');
};
const handleAddToBatch = useCallback(() => {
dispatch(imagesAddedToBatch([image.image_name]));
}, [dispatch, image.image_name]);
return (
<>
<MenuItem icon={<ExternalLinkIcon />} onClickCapture={handleOpenInNewTab}>
{t('common.openInNewTab')}
</MenuItem>
{isLightboxEnabled && (
<MenuItem icon={<FaExpand />} onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</MenuItem>
)}
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleRecallPrompt}
isDisabled={
metadata?.positive_prompt === undefined &&
metadata?.negative_prompt === undefined
}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleRecallSeed}
isDisabled={metadata?.seed === undefined}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseAllParameters}
isDisabled={!metadata}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={<FaShare />}
onClickCapture={handleSendToImageToImage}
id="send-to-img2img"
>
{t('parameters.sendToImg2Img')}
</MenuItem>
{isCanvasEnabled && (
<MenuItem
icon={<FaShare />}
onClickCapture={handleSendToCanvas}
id="send-to-canvas"
>
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
)}
<MenuItem
icon={<FaFolder />}
isDisabled={isInBatch}
onClickCapture={handleAddToBatch}
>
Add to Batch
</MenuItem>
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
{image.board_id ? 'Change Board' : 'Add to Board'}
</MenuItem>
{image.board_id && (
<MenuItem icon={<FaFolder />} onClickCapture={handleRemoveFromBoard}>
Remove from Board
</MenuItem>
)}
<MenuItem
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
icon={<FaTrash />}
onClickCapture={handleDelete}
>
{t('gallery.deleteImage')}
</MenuItem>
</>
);
};

View File

@ -0,0 +1,212 @@
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { useCallback } from 'react';
import { UnsafeImageMetadata } from 'services/api/endpoints/images';
import MetadataItem from './MetadataItem';
type Props = {
metadata?: UnsafeImageMetadata['metadata'];
};
const ImageMetadataActions = (props: Props) => {
const { metadata } = props;
const {
recallBothPrompts,
recallPositivePrompt,
recallNegativePrompt,
recallSeed,
recallInitialImage,
recallCfgScale,
recallModel,
recallScheduler,
recallSteps,
recallWidth,
recallHeight,
recallStrength,
recallAllParameters,
} = useRecallParameters();
const handleRecallPositivePrompt = useCallback(() => {
recallPositivePrompt(metadata?.positive_prompt);
}, [metadata?.positive_prompt, recallPositivePrompt]);
const handleRecallNegativePrompt = useCallback(() => {
recallNegativePrompt(metadata?.negative_prompt);
}, [metadata?.negative_prompt, recallNegativePrompt]);
const handleRecallSeed = useCallback(() => {
recallSeed(metadata?.seed);
}, [metadata?.seed, recallSeed]);
const handleRecallModel = useCallback(() => {
recallModel(metadata?.model);
}, [metadata?.model, recallModel]);
const handleRecallWidth = useCallback(() => {
recallWidth(metadata?.width);
}, [metadata?.width, recallWidth]);
const handleRecallHeight = useCallback(() => {
recallHeight(metadata?.height);
}, [metadata?.height, recallHeight]);
const handleRecallScheduler = useCallback(() => {
recallScheduler(metadata?.scheduler);
}, [metadata?.scheduler, recallScheduler]);
const handleRecallSteps = useCallback(() => {
recallSteps(metadata?.steps);
}, [metadata?.steps, recallSteps]);
const handleRecallCfgScale = useCallback(() => {
recallCfgScale(metadata?.cfg_scale);
}, [metadata?.cfg_scale, recallCfgScale]);
const handleRecallStrength = useCallback(() => {
recallStrength(metadata?.strength);
}, [metadata?.strength, recallStrength]);
if (!metadata || Object.keys(metadata).length === 0) {
return null;
}
return (
<>
{metadata.generation_mode && (
<MetadataItem
label="Generation Mode"
value={metadata.generation_mode}
/>
)}
{metadata.positive_prompt && (
<MetadataItem
label="Positive Prompt"
labelPosition="top"
value={metadata.positive_prompt}
onClick={handleRecallPositivePrompt}
/>
)}
{metadata.negative_prompt && (
<MetadataItem
label="Negative Prompt"
labelPosition="top"
value={metadata.negative_prompt}
onClick={handleRecallNegativePrompt}
/>
)}
{metadata.seed !== undefined && (
<MetadataItem
label="Seed"
value={metadata.seed}
onClick={handleRecallSeed}
/>
)}
{metadata.model !== undefined && (
<MetadataItem
label="Model"
value={metadata.model.model_name}
onClick={handleRecallModel}
/>
)}
{metadata.width && (
<MetadataItem
label="Width"
value={metadata.width}
onClick={handleRecallWidth}
/>
)}
{metadata.height && (
<MetadataItem
label="Height"
value={metadata.height}
onClick={handleRecallHeight}
/>
)}
{/* {metadata.threshold !== undefined && (
<MetadataItem
label="Noise Threshold"
value={metadata.threshold}
onClick={() => dispatch(setThreshold(Number(metadata.threshold)))}
/>
)}
{metadata.perlin !== undefined && (
<MetadataItem
label="Perlin Noise"
value={metadata.perlin}
onClick={() => dispatch(setPerlin(Number(metadata.perlin)))}
/>
)} */}
{metadata.scheduler && (
<MetadataItem
label="Scheduler"
value={metadata.scheduler}
onClick={handleRecallScheduler}
/>
)}
{metadata.steps && (
<MetadataItem
label="Steps"
value={metadata.steps}
onClick={handleRecallSteps}
/>
)}
{metadata.cfg_scale !== undefined && (
<MetadataItem
label="CFG scale"
value={metadata.cfg_scale}
onClick={handleRecallCfgScale}
/>
)}
{/* {metadata.variations && metadata.variations.length > 0 && (
<MetadataItem
label="Seed-weight pairs"
value={seedWeightsToString(metadata.variations)}
onClick={() =>
dispatch(
setSeedWeights(seedWeightsToString(metadata.variations))
)
}
/>
)}
{metadata.seamless && (
<MetadataItem
label="Seamless"
value={metadata.seamless}
onClick={() => dispatch(setSeamless(metadata.seamless))}
/>
)}
{metadata.hires_fix && (
<MetadataItem
label="High Resolution Optimization"
value={metadata.hires_fix}
onClick={() => dispatch(setHiresFix(metadata.hires_fix))}
/>
)} */}
{/* {init_image_path && (
<MetadataItem
label="Initial image"
value={init_image_path}
isLink
onClick={() => dispatch(setInitialImage(init_image_path))}
/>
)} */}
{metadata.strength && (
<MetadataItem
label="Image to image strength"
value={metadata.strength}
onClick={handleRecallStrength}
/>
)}
{/* {metadata.fit && (
<MetadataItem
label="Image to image fit"
value={metadata.fit}
onClick={() => dispatch(setShouldFitToWidthHeight(metadata.fit))}
/>
)} */}
</>
);
};
export default ImageMetadataActions;

View File

@ -1,131 +1,74 @@
import { ExternalLinkIcon } from '@chakra-ui/icons'; import { ExternalLinkIcon } from '@chakra-ui/icons';
import { import {
Box,
Center,
Flex, Flex,
IconButton,
Link, Link,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
Text, Text,
Tooltip,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks'; import { skipToken } from '@reduxjs/toolkit/dist/query';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { memo, useMemo } from 'react';
import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaCopy } from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { ImageDTO } from 'services/api/types'; import { ImageDTO } from 'services/api/types';
import { useDebounce } from 'use-debounce';
type MetadataItemProps = { import ImageMetadataActions from './ImageMetadataActions';
isLink?: boolean; import MetadataJSONViewer from './MetadataJSONViewer';
label: string;
onClick?: () => void;
value: number | string | boolean;
labelPosition?: string;
withCopy?: boolean;
};
/**
* Component to display an individual metadata item or parameter.
*/
const MetadataItem = ({
label,
value,
onClick,
isLink,
labelPosition,
withCopy = false,
}: MetadataItemProps) => {
const { t } = useTranslation();
if (!value) {
return null;
}
return (
<Flex gap={2}>
{onClick && (
<Tooltip label={`Recall ${label}`}>
<IconButton
aria-label={t('accessibility.useThisParameter')}
icon={<IoArrowUndoCircleOutline />}
size="xs"
variant="ghost"
fontSize={20}
onClick={onClick}
/>
</Tooltip>
)}
{withCopy && (
<Tooltip label={`Copy ${label}`}>
<IconButton
aria-label={`Copy ${label}`}
icon={<FaCopy />}
size="xs"
variant="ghost"
fontSize={14}
onClick={() => navigator.clipboard.writeText(value.toString())}
/>
</Tooltip>
)}
<Flex direction={labelPosition ? 'column' : 'row'}>
<Text fontWeight="semibold" whiteSpace="pre-wrap" pr={2}>
{label}:
</Text>
{isLink ? (
<Link href={value.toString()} isExternal wordBreak="break-all">
{value.toString()} <ExternalLinkIcon mx="2px" />
</Link>
) : (
<Text overflowY="scroll" wordBreak="break-all">
{value.toString()}
</Text>
)}
</Flex>
</Flex>
);
};
type ImageMetadataViewerProps = { type ImageMetadataViewerProps = {
image: ImageDTO; image: ImageDTO;
}; };
/**
* Image metadata viewer overlays currently selected image and provides
* access to any of its metadata for use in processing.
*/
const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
const dispatch = useAppDispatch(); // TODO: fix hotkeys
const { // const dispatch = useAppDispatch();
recallBothPrompts, // useHotkeys('esc', () => {
recallPositivePrompt, // dispatch(setShouldShowImageDetails(false));
recallNegativePrompt, // });
recallSeed,
recallInitialImage,
recallCfgScale,
recallModel,
recallScheduler,
recallSteps,
recallWidth,
recallHeight,
recallStrength,
recallAllParameters,
} = useRecallParameters();
useHotkeys('esc', () => { const [debouncedMetadataQueryArg, debounceState] = useDebounce(
dispatch(setShouldShowImageDetails(false)); image.image_name,
}); 500
);
const sessionId = image?.session_id; const { currentData } = useGetImageMetadataQuery(
debounceState.isPending()
? skipToken
: debouncedMetadataQueryArg ?? skipToken
);
const metadata = currentData?.metadata;
const graph = currentData?.graph;
const metadata = image?.metadata; const tabData = useMemo(() => {
const _tabData: { label: string; data: object; copyTooltip: string }[] = [];
const { t } = useTranslation(); if (metadata) {
_tabData.push({
label: 'Core Metadata',
data: metadata,
copyTooltip: 'Copy Core Metadata JSON',
});
}
const metadataJSON = JSON.stringify(image, null, 2); if (image) {
_tabData.push({
label: 'Image Details',
data: image,
copyTooltip: 'Copy Image Details JSON',
});
}
if (graph) {
_tabData.push({
label: 'Graph',
data: graph,
copyTooltip: 'Copy Graph JSON',
});
}
return _tabData;
}, [metadata, graph, image]);
return ( return (
<Flex <Flex
@ -136,11 +79,13 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
width: 'full', width: 'full',
height: 'full', height: 'full',
backdropFilter: 'blur(20px)', backdropFilter: 'blur(20px)',
bg: 'whiteAlpha.600', bg: 'baseAlpha.200',
_dark: { _dark: {
bg: 'blackAlpha.600', bg: 'blackAlpha.600',
}, },
overflow: 'scroll', borderRadius: 'base',
position: 'absolute',
overflow: 'hidden',
}} }}
> >
<Flex gap={2}> <Flex gap={2}>
@ -150,179 +95,42 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
<ExternalLinkIcon mx="2px" /> <ExternalLinkIcon mx="2px" />
</Link> </Link>
</Flex> </Flex>
{metadata && Object.keys(metadata).length > 0 ? (
<>
{metadata.type && (
<MetadataItem label="Invocation type" value={metadata.type} />
)}
{sessionId && <MetadataItem label="Session ID" value={sessionId} />}
{metadata.positive_conditioning && (
<MetadataItem
label="Positive Prompt"
labelPosition="top"
value={metadata.positive_conditioning}
onClick={() =>
recallPositivePrompt(metadata.positive_conditioning)
}
/>
)}
{metadata.negative_conditioning && (
<MetadataItem
label="Negative Prompt"
labelPosition="top"
value={metadata.negative_conditioning}
onClick={() =>
recallNegativePrompt(metadata.negative_conditioning)
}
/>
)}
{metadata.seed !== undefined && (
<MetadataItem
label="Seed"
value={metadata.seed}
onClick={() => recallSeed(metadata.seed)}
/>
)}
{metadata.model !== undefined && (
<MetadataItem
label="Model"
value={metadata.model}
onClick={() => recallModel(metadata.model)}
/>
)}
{metadata.width && (
<MetadataItem
label="Width"
value={metadata.width}
onClick={() => recallWidth(metadata.width)}
/>
)}
{metadata.height && (
<MetadataItem
label="Height"
value={metadata.height}
onClick={() => recallHeight(metadata.height)}
/>
)}
{/* {metadata.threshold !== undefined && (
<MetadataItem
label="Noise Threshold"
value={metadata.threshold}
onClick={() => dispatch(setThreshold(Number(metadata.threshold)))}
/>
)}
{metadata.perlin !== undefined && (
<MetadataItem
label="Perlin Noise"
value={metadata.perlin}
onClick={() => dispatch(setPerlin(Number(metadata.perlin)))}
/>
)} */}
{metadata.scheduler && (
<MetadataItem
label="Scheduler"
value={metadata.scheduler}
onClick={() => recallScheduler(metadata.scheduler)}
/>
)}
{metadata.steps && (
<MetadataItem
label="Steps"
value={metadata.steps}
onClick={() => recallSteps(metadata.steps)}
/>
)}
{metadata.cfg_scale !== undefined && (
<MetadataItem
label="CFG scale"
value={metadata.cfg_scale}
onClick={() => recallCfgScale(metadata.cfg_scale)}
/>
)}
{/* {metadata.variations && metadata.variations.length > 0 && (
<MetadataItem
label="Seed-weight pairs"
value={seedWeightsToString(metadata.variations)}
onClick={() =>
dispatch(
setSeedWeights(seedWeightsToString(metadata.variations))
)
}
/>
)}
{metadata.seamless && (
<MetadataItem
label="Seamless"
value={metadata.seamless}
onClick={() => dispatch(setSeamless(metadata.seamless))}
/>
)}
{metadata.hires_fix && (
<MetadataItem
label="High Resolution Optimization"
value={metadata.hires_fix}
onClick={() => dispatch(setHiresFix(metadata.hires_fix))}
/>
)} */}
{/* {init_image_path && ( <ImageMetadataActions metadata={metadata} />
<MetadataItem
label="Initial image" <Tabs
value={init_image_path} variant="line"
isLink sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}
onClick={() => dispatch(setInitialImage(init_image_path))} >
/> <TabList>
)} */} {tabData.map((tab) => (
{metadata.strength && ( <Tab
<MetadataItem key={tab.label}
label="Image to image strength" sx={{
value={metadata.strength} borderTopRadius: 'base',
onClick={() => recallStrength(metadata.strength)} }}
/> >
)} <Text sx={{ color: 'base.700', _dark: { color: 'base.300' } }}>
{/* {metadata.fit && ( {tab.label}
<MetadataItem </Text>
label="Image to image fit" </Tab>
value={metadata.fit} ))}
onClick={() => dispatch(setShouldFitToWidthHeight(metadata.fit))} </TabList>
/>
)} */} <TabPanels sx={{ w: 'full', h: 'full' }}>
</> {tabData.map((tab) => (
) : ( <TabPanel
<Center width="100%" pt={10}> key={tab.label}
<Text fontSize="lg" fontWeight="semibold"> sx={{ w: 'full', h: 'full', p: 0, pt: 4 }}
No metadata available >
</Text> <MetadataJSONViewer
</Center> jsonObject={tab.data}
)} copyTooltip={tab.copyTooltip}
<Flex gap={2} direction="column" overflow="auto"> />
<Flex gap={2}> </TabPanel>
<Tooltip label="Copy metadata JSON"> ))}
<IconButton </TabPanels>
aria-label={t('accessibility.copyMetadataJson')} </Tabs>
icon={<FaCopy />}
size="xs"
variant="ghost"
fontSize={14}
onClick={() => navigator.clipboard.writeText(metadataJSON)}
/>
</Tooltip>
<Text fontWeight="semibold">Metadata JSON:</Text>
</Flex>
<OverlayScrollbarsComponent defer>
<Box
sx={{
padding: 4,
borderRadius: 'base',
bg: 'whiteAlpha.500',
_dark: { bg: 'blackAlpha.500' },
w: 'full',
}}
>
<pre>{metadataJSON}</pre>
</Box>
</OverlayScrollbarsComponent>
</Flex>
</Flex> </Flex>
); );
}; };

View File

@ -0,0 +1,77 @@
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { Flex, IconButton, Link, Text, Tooltip } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { FaCopy } from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
type MetadataItemProps = {
isLink?: boolean;
label: string;
onClick?: () => void;
value: number | string | boolean;
labelPosition?: string;
withCopy?: boolean;
};
/**
* Component to display an individual metadata item or parameter.
*/
const MetadataItem = ({
label,
value,
onClick,
isLink,
labelPosition,
withCopy = false,
}: MetadataItemProps) => {
const { t } = useTranslation();
if (!value) {
return null;
}
return (
<Flex gap={2}>
{onClick && (
<Tooltip label={`Recall ${label}`}>
<IconButton
aria-label={t('accessibility.useThisParameter')}
icon={<IoArrowUndoCircleOutline />}
size="xs"
variant="ghost"
fontSize={20}
onClick={onClick}
/>
</Tooltip>
)}
{withCopy && (
<Tooltip label={`Copy ${label}`}>
<IconButton
aria-label={`Copy ${label}`}
icon={<FaCopy />}
size="xs"
variant="ghost"
fontSize={14}
onClick={() => navigator.clipboard.writeText(value.toString())}
/>
</Tooltip>
)}
<Flex direction={labelPosition ? 'column' : 'row'}>
<Text fontWeight="semibold" whiteSpace="pre-wrap" pr={2}>
{label}:
</Text>
{isLink ? (
<Link href={value.toString()} isExternal wordBreak="break-all">
{value.toString()} <ExternalLinkIcon mx="2px" />
</Link>
) : (
<Text overflowY="scroll" wordBreak="break-all">
{value.toString()}
</Text>
)}
</Flex>
</Flex>
);
};
export default MetadataItem;

View File

@ -0,0 +1,70 @@
import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { useMemo } from 'react';
import { FaCopy } from 'react-icons/fa';
type Props = {
copyTooltip: string;
jsonObject: object;
};
const MetadataJSONViewer = (props: Props) => {
const { copyTooltip, jsonObject } = props;
const jsonString = useMemo(
() => JSON.stringify(jsonObject, null, 2),
[jsonObject]
);
return (
<Flex
sx={{
borderRadius: 'base',
bg: 'whiteAlpha.500',
_dark: { bg: 'blackAlpha.500' },
flexGrow: 1,
w: 'full',
h: 'full',
position: 'relative',
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'auto',
p: 4,
}}
>
<OverlayScrollbarsComponent
defer
style={{ height: '100%', width: '100%' }}
options={{
scrollbars: {
visibility: 'auto',
autoHide: 'move',
autoHideDelay: 1300,
theme: 'os-theme-dark',
},
}}
>
<pre>{jsonString}</pre>
</OverlayScrollbarsComponent>
</Box>
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
<Tooltip label={copyTooltip}>
<IconButton
aria-label={copyTooltip}
icon={<FaCopy />}
variant="ghost"
onClick={() => navigator.clipboard.writeText(jsonString)}
/>
</Tooltip>
</Flex>
</Flex>
);
};
export default MetadataJSONViewer;

View File

@ -1,18 +1,8 @@
import { ChakraProps, Flex, Grid, IconButton, Spinner } from '@chakra-ui/react'; import { ChakraProps, Flex, Grid, IconButton, Spinner } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
imageSelected,
selectFilteredImages,
selectImagesById,
} from 'features/gallery/store/gallerySlice';
import { clamp, isEqual } from 'lodash-es';
import { memo, useCallback, useState } from 'react'; import { memo, useCallback, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaAngleDoubleRight, FaAngleLeft, FaAngleRight } from 'react-icons/fa'; import { FaAngleDoubleRight, FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { receivedPageOfImages } from 'services/api/thunks/image'; import { useNextPrevImage } from '../hooks/useNextPrevImage';
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = { const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
height: '100%', height: '100%',
@ -24,74 +14,18 @@ const nextPrevButtonStyles: ChakraProps['sx'] = {
color: 'base.100', color: 'base.100',
}; };
export const nextPrevImageButtonsSelector = createSelector(
[stateSelector, selectFilteredImages],
(state, filteredImages) => {
const { total, isFetching } = state.gallery;
const lastSelectedImage =
state.gallery.selection[state.gallery.selection.length - 1];
if (!lastSelectedImage || filteredImages.length === 0) {
return {
isOnFirstImage: true,
isOnLastImage: true,
};
}
const currentImageIndex = filteredImages.findIndex(
(i) => i.image_name === lastSelectedImage
);
const nextImageIndex = clamp(
currentImageIndex + 1,
0,
filteredImages.length - 1
);
const prevImageIndex = clamp(
currentImageIndex - 1,
0,
filteredImages.length - 1
);
const nextImageId = filteredImages[nextImageIndex].image_name;
const prevImageId = filteredImages[prevImageIndex].image_name;
const nextImage = selectImagesById(state, nextImageId);
const prevImage = selectImagesById(state, prevImageId);
const imagesLength = filteredImages.length;
return {
isOnFirstImage: currentImageIndex === 0,
isOnLastImage:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
areMoreImagesAvailable: total > imagesLength,
isFetching,
nextImage,
prevImage,
nextImageId,
prevImageId,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
const NextPrevImageButtons = () => { const NextPrevImageButtons = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
handlePrevImage,
handleNextImage,
isOnFirstImage, isOnFirstImage,
isOnLastImage, isOnLastImage,
nextImageId, handleLoadMoreImages,
prevImageId,
areMoreImagesAvailable, areMoreImagesAvailable,
isFetching, isFetching,
} = useAppSelector(nextPrevImageButtonsSelector); } = useNextPrevImage();
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState<boolean>(false); useState<boolean>(false);
@ -104,50 +38,6 @@ const NextPrevImageButtons = () => {
setShouldShowNextPrevButtons(false); setShouldShowNextPrevButtons(false);
}, []); }, []);
const handlePrevImage = useCallback(() => {
prevImageId && dispatch(imageSelected(prevImageId));
}, [dispatch, prevImageId]);
const handleNextImage = useCallback(() => {
nextImageId && dispatch(imageSelected(nextImageId));
}, [dispatch, nextImageId]);
const handleLoadMoreImages = useCallback(() => {
dispatch(
receivedPageOfImages({
is_intermediate: false,
})
);
}, [dispatch]);
useHotkeys(
'left',
() => {
handlePrevImage();
},
[prevImageId]
);
useHotkeys(
'right',
() => {
if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
handleLoadMoreImages();
return;
}
if (!isOnLastImage) {
handleNextImage();
}
},
[
nextImageId,
isOnLastImage,
areMoreImagesAvailable,
handleLoadMoreImages,
isFetching,
]
);
return ( return (
<Flex <Flex
sx={{ sx={{

View File

@ -0,0 +1,108 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
imageSelected,
selectFilteredImages,
selectImagesById,
} from 'features/gallery/store/gallerySlice';
import { clamp, isEqual } from 'lodash-es';
import { useCallback } from 'react';
import { receivedPageOfImages } from 'services/api/thunks/image';
export const nextPrevImageButtonsSelector = createSelector(
[stateSelector, selectFilteredImages],
(state, filteredImages) => {
const { total, isFetching } = state.gallery;
const lastSelectedImage =
state.gallery.selection[state.gallery.selection.length - 1];
if (!lastSelectedImage || filteredImages.length === 0) {
return {
isOnFirstImage: true,
isOnLastImage: true,
};
}
const currentImageIndex = filteredImages.findIndex(
(i) => i.image_name === lastSelectedImage
);
const nextImageIndex = clamp(
currentImageIndex + 1,
0,
filteredImages.length - 1
);
const prevImageIndex = clamp(
currentImageIndex - 1,
0,
filteredImages.length - 1
);
const nextImageId = filteredImages[nextImageIndex].image_name;
const prevImageId = filteredImages[prevImageIndex].image_name;
const nextImage = selectImagesById(state, nextImageId);
const prevImage = selectImagesById(state, prevImageId);
const imagesLength = filteredImages.length;
return {
isOnFirstImage: currentImageIndex === 0,
isOnLastImage:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
areMoreImagesAvailable: total > imagesLength,
isFetching,
nextImage,
prevImage,
nextImageId,
prevImageId,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
export const useNextPrevImage = () => {
const dispatch = useAppDispatch();
const {
isOnFirstImage,
isOnLastImage,
nextImageId,
prevImageId,
areMoreImagesAvailable,
isFetching,
} = useAppSelector(nextPrevImageButtonsSelector);
const handlePrevImage = useCallback(() => {
prevImageId && dispatch(imageSelected(prevImageId));
}, [dispatch, prevImageId]);
const handleNextImage = useCallback(() => {
nextImageId && dispatch(imageSelected(nextImageId));
}, [dispatch, nextImageId]);
const handleLoadMoreImages = useCallback(() => {
dispatch(
receivedPageOfImages({
is_intermediate: false,
})
);
}, [dispatch]);
return {
handlePrevImage,
handleNextImage,
isOnFirstImage,
isOnLastImage,
nextImageId,
prevImageId,
areMoreImagesAvailable,
handleLoadMoreImages,
isFetching,
};
};

View File

@ -45,7 +45,7 @@ const ParamLoraSelect = () => {
data.push({ data.push({
value: id, value: id,
label: lora.name, label: lora.model_name,
disabled, disabled,
group: MODEL_TYPE_MAP[lora.base_model], group: MODEL_TYPE_MAP[lora.base_model],
tooltip: disabled tooltip: disabled

View File

@ -6,6 +6,7 @@ import LoadNodesButton from '../ui/LoadNodesButton';
import NodeInvokeButton from '../ui/NodeInvokeButton'; import NodeInvokeButton from '../ui/NodeInvokeButton';
import ReloadSchemaButton from '../ui/ReloadSchemaButton'; import ReloadSchemaButton from '../ui/ReloadSchemaButton';
import SaveNodesButton from '../ui/SaveNodesButton'; import SaveNodesButton from '../ui/SaveNodesButton';
import ClearNodesButton from '../ui/ClearNodesButton';
const TopCenterPanel = () => { const TopCenterPanel = () => {
return ( return (
@ -16,6 +17,7 @@ const TopCenterPanel = () => {
<ReloadSchemaButton /> <ReloadSchemaButton />
<SaveNodesButton /> <SaveNodesButton />
<LoadNodesButton /> <LoadNodesButton />
<ClearNodesButton />
</HStack> </HStack>
</Panel> </Panel>
); );

View File

@ -0,0 +1,86 @@
import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Button,
Text,
useDisclosure,
} from '@chakra-ui/react';
import { makeToast } from 'app/components/Toaster';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { clearNodes } from 'features/nodes/store/nodesSlice';
import { addToast } from 'features/system/store/systemSlice';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
const ClearNodesButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
const cancelRef = useRef<HTMLButtonElement | null>(null);
const nodes = useAppSelector((state: RootState) => state.nodes.nodes);
const handleConfirmClear = () => {
dispatch(clearNodes());
dispatch(
addToast(
makeToast({
title: t('toast.nodesCleared'),
status: 'success',
})
)
);
onClose();
};
return (
<>
<IAIIconButton
icon={<FaTrash />}
tooltip={t('nodes.clearNodes')}
aria-label={t('nodes.clearNodes')}
onClick={onOpen}
isDisabled={nodes.length === 0}
/>
<AlertDialog
isOpen={isOpen}
onClose={onClose}
leastDestructiveRef={cancelRef}
isCentered
>
<AlertDialogOverlay />
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{t('nodes.clearNodes')}
</AlertDialogHeader>
<AlertDialogBody>
<Text>{t('common.clearNodes')}</Text>
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="red" ml={3} onClick={handleConfirmClear}>
{t('common.accept')}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
export default memo(ClearNodesButton);

View File

@ -12,6 +12,8 @@ const SaveNodesButton = () => {
(state: RootState) => state.nodes.editorInstance (state: RootState) => state.nodes.editorInstance
); );
const nodes = useAppSelector((state: RootState) => state.nodes.nodes);
const saveEditorToJSON = useCallback(() => { const saveEditorToJSON = useCallback(() => {
if (editorInstance) { if (editorInstance) {
const editorState = editorInstance.toObject(); const editorState = editorInstance.toObject();
@ -38,6 +40,7 @@ const SaveNodesButton = () => {
tooltip={t('nodes.saveNodes')} tooltip={t('nodes.saveNodes')}
aria-label={t('nodes.saveNodes')} aria-label={t('nodes.saveNodes')}
onClick={saveEditorToJSON} onClick={saveEditorToJSON}
isDisabled={nodes.length === 0}
/> />
); );
}; };

View File

@ -133,6 +133,10 @@ const nodesSlice = createSlice({
loadFileEdges: (state, action: PayloadAction<Edge[]>) => { loadFileEdges: (state, action: PayloadAction<Edge[]>) => {
state.edges = action.payload; state.edges = action.payload;
}, },
clearNodes: (state) => {
state.nodes = [];
state.edges = [];
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => { builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
@ -156,6 +160,7 @@ export const {
setEditorInstance, setEditorInstance,
loadFileNodes, loadFileNodes,
loadFileEdges, loadFileEdges,
clearNodes,
} = nodesSlice.actions; } = nodesSlice.actions;
export default nodesSlice.reducer; export default nodesSlice.reducer;

View File

@ -1,94 +0,0 @@
import { RootState } from 'app/store/store';
import { getValidControlNets } from 'features/controlNet/util/getValidControlNets';
import { CollectInvocation, ControlNetInvocation } from 'services/api/types';
import { NonNullableGraph } from '../types/types';
import { CONTROL_NET_COLLECT } from './graphBuilders/constants';
export const addControlNetToLinearGraph = (
graph: NonNullableGraph,
baseNodeId: string,
state: RootState
): void => {
const { isEnabled: isControlNetEnabled, controlNets } = state.controlNet;
const validControlNets = getValidControlNets(controlNets);
if (isControlNetEnabled && Boolean(validControlNets.length)) {
if (validControlNets.length > 1) {
// We have multiple controlnets, add ControlNet collector
const controlNetIterateNode: CollectInvocation = {
id: CONTROL_NET_COLLECT,
type: 'collect',
};
graph.nodes[controlNetIterateNode.id] = controlNetIterateNode;
graph.edges.push({
source: { node_id: controlNetIterateNode.id, field: 'collection' },
destination: {
node_id: baseNodeId,
field: 'control',
},
});
}
validControlNets.forEach((controlNet) => {
const {
controlNetId,
controlImage,
processedControlImage,
beginStepPct,
endStepPct,
controlMode,
model,
processorType,
weight,
} = controlNet;
const controlNetNode: ControlNetInvocation = {
id: `control_net_${controlNetId}`,
type: 'controlnet',
begin_step_percent: beginStepPct,
end_step_percent: endStepPct,
control_mode: controlMode,
control_model: model as ControlNetInvocation['control_model'],
control_weight: weight,
};
if (processedControlImage && processorType !== 'none') {
// We've already processed the image in the app, so we can just use the processed image
controlNetNode.image = {
image_name: processedControlImage,
};
} else if (controlImage) {
// The control image is preprocessed
controlNetNode.image = {
image_name: controlImage,
};
} else {
// Skip ControlNets without an unprocessed image - should never happen if everything is working correctly
return;
}
graph.nodes[controlNetNode.id] = controlNetNode;
if (validControlNets.length > 1) {
// if we have multiple controlnets, link to the collector
graph.edges.push({
source: { node_id: controlNetNode.id, field: 'control' },
destination: {
node_id: CONTROL_NET_COLLECT,
field: 'item',
},
});
} else {
// otherwise, link directly to the base node
graph.edges.push({
source: { node_id: controlNetNode.id, field: 'control' },
destination: {
node_id: baseNodeId,
field: 'control',
},
});
}
});
}
};

View File

@ -1,40 +0,0 @@
import {
Edge,
ImageToImageInvocation,
InpaintInvocation,
IterateInvocation,
RandomRangeInvocation,
RangeInvocation,
TextToImageInvocation,
} from 'services/api/types';
export const buildEdges = (
baseNode: TextToImageInvocation | ImageToImageInvocation | InpaintInvocation,
rangeNode: RangeInvocation | RandomRangeInvocation,
iterateNode: IterateInvocation
): Edge[] => {
const edges: Edge[] = [
{
source: {
node_id: rangeNode.id,
field: 'collection',
},
destination: {
node_id: iterateNode.id,
field: 'collection',
},
},
{
source: {
node_id: iterateNode.id,
field: 'item',
},
destination: {
node_id: baseNode.id,
field: 'seed',
},
},
];
return edges;
};

View File

@ -0,0 +1,100 @@
import { RootState } from 'app/store/store';
import { getValidControlNets } from 'features/controlNet/util/getValidControlNets';
import { omit } from 'lodash-es';
import {
CollectInvocation,
ControlField,
ControlNetInvocation,
MetadataAccumulatorInvocation,
} from 'services/api/types';
import { NonNullableGraph } from '../../types/types';
import { CONTROL_NET_COLLECT, METADATA_ACCUMULATOR } from './constants';
export const addControlNetToLinearGraph = (
state: RootState,
graph: NonNullableGraph,
baseNodeId: string
): void => {
const { isEnabled: isControlNetEnabled, controlNets } = state.controlNet;
const validControlNets = getValidControlNets(controlNets);
const metadataAccumulator = graph.nodes[
METADATA_ACCUMULATOR
] as MetadataAccumulatorInvocation;
if (isControlNetEnabled && Boolean(validControlNets.length)) {
if (validControlNets.length) {
// We have multiple controlnets, add ControlNet collector
const controlNetIterateNode: CollectInvocation = {
id: CONTROL_NET_COLLECT,
type: 'collect',
};
graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode;
graph.edges.push({
source: { node_id: CONTROL_NET_COLLECT, field: 'collection' },
destination: {
node_id: baseNodeId,
field: 'control',
},
});
validControlNets.forEach((controlNet) => {
const {
controlNetId,
controlImage,
processedControlImage,
beginStepPct,
endStepPct,
controlMode,
model,
processorType,
weight,
} = controlNet;
const controlNetNode: ControlNetInvocation = {
id: `control_net_${controlNetId}`,
type: 'controlnet',
begin_step_percent: beginStepPct,
end_step_percent: endStepPct,
control_mode: controlMode,
control_model: model as ControlNetInvocation['control_model'],
control_weight: weight,
};
if (processedControlImage && processorType !== 'none') {
// We've already processed the image in the app, so we can just use the processed image
controlNetNode.image = {
image_name: processedControlImage,
};
} else if (controlImage) {
// The control image is preprocessed
controlNetNode.image = {
image_name: controlImage,
};
} else {
// Skip ControlNets without an unprocessed image - should never happen if everything is working correctly
return;
}
graph.nodes[controlNetNode.id] = controlNetNode;
// metadata accumulator only needs a control field - not the whole node
// extract what we need and add to the accumulator
const controlField = omit(controlNetNode, [
'id',
'type',
]) as ControlField;
metadataAccumulator.controlnets.push(controlField);
graph.edges.push({
source: { node_id: controlNetNode.id, field: 'control' },
destination: {
node_id: CONTROL_NET_COLLECT,
field: 'item',
},
});
});
}
}
};

View File

@ -1,8 +1,10 @@
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { NonNullableGraph } from 'features/nodes/types/types'; import { NonNullableGraph } from 'features/nodes/types/types';
import { unset } from 'lodash-es';
import { import {
DynamicPromptInvocation, DynamicPromptInvocation,
IterateInvocation, IterateInvocation,
MetadataAccumulatorInvocation,
NoiseInvocation, NoiseInvocation,
RandomIntInvocation, RandomIntInvocation,
RangeOfSizeInvocation, RangeOfSizeInvocation,
@ -10,16 +12,16 @@ import {
import { import {
DYNAMIC_PROMPT, DYNAMIC_PROMPT,
ITERATE, ITERATE,
METADATA_ACCUMULATOR,
NOISE, NOISE,
POSITIVE_CONDITIONING, POSITIVE_CONDITIONING,
RANDOM_INT, RANDOM_INT,
RANGE_OF_SIZE, RANGE_OF_SIZE,
} from './constants'; } from './constants';
import { unset } from 'lodash-es';
export const addDynamicPromptsToGraph = ( export const addDynamicPromptsToGraph = (
graph: NonNullableGraph, state: RootState,
state: RootState graph: NonNullableGraph
): void => { ): void => {
const { positivePrompt, iterations, seed, shouldRandomizeSeed } = const { positivePrompt, iterations, seed, shouldRandomizeSeed } =
state.generation; state.generation;
@ -30,6 +32,10 @@ export const addDynamicPromptsToGraph = (
maxPrompts, maxPrompts,
} = state.dynamicPrompts; } = state.dynamicPrompts;
const metadataAccumulator = graph.nodes[
METADATA_ACCUMULATOR
] as MetadataAccumulatorInvocation;
if (isDynamicPromptsEnabled) { if (isDynamicPromptsEnabled) {
// iteration is handled via dynamic prompts // iteration is handled via dynamic prompts
unset(graph.nodes[POSITIVE_CONDITIONING], 'prompt'); unset(graph.nodes[POSITIVE_CONDITIONING], 'prompt');
@ -74,6 +80,18 @@ export const addDynamicPromptsToGraph = (
} }
); );
// hook up positive prompt to metadata
graph.edges.push({
source: {
node_id: ITERATE,
field: 'item',
},
destination: {
node_id: METADATA_ACCUMULATOR,
field: 'positive_prompt',
},
});
if (shouldRandomizeSeed) { if (shouldRandomizeSeed) {
// Random int node to generate the starting seed // Random int node to generate the starting seed
const randomIntNode: RandomIntInvocation = { const randomIntNode: RandomIntInvocation = {
@ -88,11 +106,22 @@ export const addDynamicPromptsToGraph = (
source: { node_id: RANDOM_INT, field: 'a' }, source: { node_id: RANDOM_INT, field: 'a' },
destination: { node_id: NOISE, field: 'seed' }, destination: { node_id: NOISE, field: 'seed' },
}); });
graph.edges.push({
source: { node_id: RANDOM_INT, field: 'a' },
destination: { node_id: METADATA_ACCUMULATOR, field: 'seed' },
});
} else { } else {
// User specified seed, so set the start of the range of size to the seed // User specified seed, so set the start of the range of size to the seed
(graph.nodes[NOISE] as NoiseInvocation).seed = seed; (graph.nodes[NOISE] as NoiseInvocation).seed = seed;
// hook up seed to metadata
metadataAccumulator.seed = seed;
} }
} else { } else {
// no dynamic prompt - hook up positive prompt
metadataAccumulator.positive_prompt = positivePrompt;
const rangeOfSizeNode: RangeOfSizeInvocation = { const rangeOfSizeNode: RangeOfSizeInvocation = {
id: RANGE_OF_SIZE, id: RANGE_OF_SIZE,
type: 'range_of_size', type: 'range_of_size',
@ -130,6 +159,18 @@ export const addDynamicPromptsToGraph = (
}, },
}); });
// hook up seed to metadata
graph.edges.push({
source: {
node_id: ITERATE,
field: 'item',
},
destination: {
node_id: METADATA_ACCUMULATOR,
field: 'seed',
},
});
// handle seed // handle seed
if (shouldRandomizeSeed) { if (shouldRandomizeSeed) {
// Random int node to generate the starting seed // Random int node to generate the starting seed

View File

@ -1,19 +1,23 @@
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { NonNullableGraph } from 'features/nodes/types/types'; import { NonNullableGraph } from 'features/nodes/types/types';
import { forEach, size } from 'lodash-es'; import { forEach, size } from 'lodash-es';
import { LoraLoaderInvocation } from 'services/api/types'; import {
LoraLoaderInvocation,
MetadataAccumulatorInvocation,
} from 'services/api/types';
import { modelIdToLoRAModelField } from '../modelIdToLoRAName'; import { modelIdToLoRAModelField } from '../modelIdToLoRAName';
import { import {
CLIP_SKIP, CLIP_SKIP,
LORA_LOADER, LORA_LOADER,
MAIN_MODEL_LOADER, MAIN_MODEL_LOADER,
METADATA_ACCUMULATOR,
NEGATIVE_CONDITIONING, NEGATIVE_CONDITIONING,
POSITIVE_CONDITIONING, POSITIVE_CONDITIONING,
} from './constants'; } from './constants';
export const addLoRAsToGraph = ( export const addLoRAsToGraph = (
graph: NonNullableGraph,
state: RootState, state: RootState,
graph: NonNullableGraph,
baseNodeId: string baseNodeId: string
): void => { ): void => {
/** /**
@ -26,6 +30,9 @@ export const addLoRAsToGraph = (
const { loras } = state.lora; const { loras } = state.lora;
const loraCount = size(loras); const loraCount = size(loras);
const metadataAccumulator = graph.nodes[
METADATA_ACCUMULATOR
] as MetadataAccumulatorInvocation;
if (loraCount > 0) { if (loraCount > 0) {
// Remove MAIN_MODEL_LOADER unet connection to feed it to LoRAs // Remove MAIN_MODEL_LOADER unet connection to feed it to LoRAs
@ -62,6 +69,10 @@ export const addLoRAsToGraph = (
weight, weight,
}; };
// add the lora to the metadata accumulator
metadataAccumulator.loras.push({ lora: loraField, weight });
// add to graph
graph.nodes[currentLoraNodeId] = loraLoaderNode; graph.nodes[currentLoraNodeId] = loraLoaderNode;
if (currentLoraIndex === 0) { if (currentLoraIndex === 0) {

View File

@ -1,5 +1,6 @@
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { NonNullableGraph } from 'features/nodes/types/types'; import { NonNullableGraph } from 'features/nodes/types/types';
import { MetadataAccumulatorInvocation } from 'services/api/types';
import { modelIdToVAEModelField } from '../modelIdToVAEModelField'; import { modelIdToVAEModelField } from '../modelIdToVAEModelField';
import { import {
IMAGE_TO_IMAGE_GRAPH, IMAGE_TO_IMAGE_GRAPH,
@ -8,18 +9,22 @@ import {
INPAINT_GRAPH, INPAINT_GRAPH,
LATENTS_TO_IMAGE, LATENTS_TO_IMAGE,
MAIN_MODEL_LOADER, MAIN_MODEL_LOADER,
METADATA_ACCUMULATOR,
TEXT_TO_IMAGE_GRAPH, TEXT_TO_IMAGE_GRAPH,
VAE_LOADER, VAE_LOADER,
} from './constants'; } from './constants';
export const addVAEToGraph = ( export const addVAEToGraph = (
graph: NonNullableGraph, state: RootState,
state: RootState graph: NonNullableGraph
): void => { ): void => {
const { vae } = state.generation; const { vae } = state.generation;
const vae_model = modelIdToVAEModelField(vae?.id || ''); const vae_model = modelIdToVAEModelField(vae?.id || '');
const isAutoVae = !vae; const isAutoVae = !vae;
const metadataAccumulator = graph.nodes[
METADATA_ACCUMULATOR
] as MetadataAccumulatorInvocation;
if (!isAutoVae) { if (!isAutoVae) {
graph.nodes[VAE_LOADER] = { graph.nodes[VAE_LOADER] = {
@ -67,4 +72,8 @@ export const addVAEToGraph = (
}, },
}); });
} }
if (vae) {
metadataAccumulator.vae = vae_model;
}
}; };

View File

@ -7,8 +7,7 @@ import {
ImageResizeInvocation, ImageResizeInvocation,
ImageToLatentsInvocation, ImageToLatentsInvocation,
} from 'services/api/types'; } from 'services/api/types';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
import { modelIdToMainModelField } from '../modelIdToMainModelField';
import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph'; import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph';
import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addLoRAsToGraph } from './addLoRAsToGraph';
import { addVAEToGraph } from './addVAEToGraph'; import { addVAEToGraph } from './addVAEToGraph';
@ -19,6 +18,7 @@ import {
LATENTS_TO_IMAGE, LATENTS_TO_IMAGE,
LATENTS_TO_LATENTS, LATENTS_TO_LATENTS,
MAIN_MODEL_LOADER, MAIN_MODEL_LOADER,
METADATA_ACCUMULATOR,
NEGATIVE_CONDITIONING, NEGATIVE_CONDITIONING,
NOISE, NOISE,
POSITIVE_CONDITIONING, POSITIVE_CONDITIONING,
@ -37,7 +37,7 @@ export const buildCanvasImageToImageGraph = (
const { const {
positivePrompt, positivePrompt,
negativePrompt, negativePrompt,
model: currentModel, model,
cfgScale: cfg_scale, cfgScale: cfg_scale,
scheduler, scheduler,
steps, steps,
@ -50,7 +50,10 @@ export const buildCanvasImageToImageGraph = (
// The bounding box determines width and height, not the width and height params // The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions; const { width, height } = state.canvas.boundingBoxDimensions;
const model = modelIdToMainModelField(currentModel?.id || ''); if (!model) {
moduleLog.error('No model found in state');
throw new Error('No model found in state');
}
const use_cpu = shouldUseNoiseSettings const use_cpu = shouldUseNoiseSettings
? shouldUseCpuNoise ? shouldUseCpuNoise
@ -275,16 +278,51 @@ export const buildCanvasImageToImageGraph = (
}); });
} }
addLoRAsToGraph(graph, state, LATENTS_TO_LATENTS); // add metadata accumulator, which is only mostly populated - some fields are added later
graph.nodes[METADATA_ACCUMULATOR] = {
id: METADATA_ACCUMULATOR,
type: 'metadata_accumulator',
generation_mode: 'img2img',
cfg_scale,
height,
width,
positive_prompt: '', // set in addDynamicPromptsToGraph
negative_prompt: negativePrompt,
model,
seed: 0, // set in addDynamicPromptsToGraph
steps,
rand_device: use_cpu ? 'cpu' : 'cuda',
scheduler,
vae: undefined, // option; set in addVAEToGraph
controlnets: [], // populated in addControlNetToLinearGraph
loras: [], // populated in addLoRAsToGraph
clip_skip: clipSkip,
strength,
init_image: initialImage.image_name,
};
// Add VAE graph.edges.push({
addVAEToGraph(graph, state); source: {
node_id: METADATA_ACCUMULATOR,
field: 'metadata',
},
destination: {
node_id: LATENTS_TO_IMAGE,
field: 'metadata',
},
});
// add dynamic prompts, mutating `graph` // add LoRA support
addDynamicPromptsToGraph(graph, state); addLoRAsToGraph(state, graph, LATENTS_TO_LATENTS);
// optionally add custom VAE
addVAEToGraph(state, graph);
// add dynamic prompts - also sets up core iteration and seed
addDynamicPromptsToGraph(state, graph);
// add controlnet, mutating `graph` // add controlnet, mutating `graph`
addControlNetToLinearGraph(graph, LATENTS_TO_LATENTS, state); addControlNetToLinearGraph(state, graph, LATENTS_TO_LATENTS);
return graph; return graph;
}; };

View File

@ -212,10 +212,10 @@ export const buildCanvasInpaintGraph = (
], ],
}; };
addLoRAsToGraph(graph, state, INPAINT); addLoRAsToGraph(state, graph, INPAINT);
// Add VAE // Add VAE
addVAEToGraph(graph, state); addVAEToGraph(state, graph);
// handle seed // handle seed
if (shouldRandomizeSeed) { if (shouldRandomizeSeed) {

View File

@ -1,8 +1,8 @@
import { log } from 'app/logging/useLogger';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { NonNullableGraph } from 'features/nodes/types/types'; import { NonNullableGraph } from 'features/nodes/types/types';
import { initialGenerationState } from 'features/parameters/store/generationSlice'; import { initialGenerationState } from 'features/parameters/store/generationSlice';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
import { modelIdToMainModelField } from '../modelIdToMainModelField';
import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph'; import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph';
import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addLoRAsToGraph } from './addLoRAsToGraph';
import { addVAEToGraph } from './addVAEToGraph'; import { addVAEToGraph } from './addVAEToGraph';
@ -10,6 +10,7 @@ import {
CLIP_SKIP, CLIP_SKIP,
LATENTS_TO_IMAGE, LATENTS_TO_IMAGE,
MAIN_MODEL_LOADER, MAIN_MODEL_LOADER,
METADATA_ACCUMULATOR,
NEGATIVE_CONDITIONING, NEGATIVE_CONDITIONING,
NOISE, NOISE,
POSITIVE_CONDITIONING, POSITIVE_CONDITIONING,
@ -17,6 +18,8 @@ import {
TEXT_TO_LATENTS, TEXT_TO_LATENTS,
} from './constants'; } from './constants';
const moduleLog = log.child({ namespace: 'nodes' });
/** /**
* Builds the Canvas tab's Text to Image graph. * Builds the Canvas tab's Text to Image graph.
*/ */
@ -26,7 +29,7 @@ export const buildCanvasTextToImageGraph = (
const { const {
positivePrompt, positivePrompt,
negativePrompt, negativePrompt,
model: currentModel, model,
cfgScale: cfg_scale, cfgScale: cfg_scale,
scheduler, scheduler,
steps, steps,
@ -38,7 +41,10 @@ export const buildCanvasTextToImageGraph = (
// The bounding box determines width and height, not the width and height params // The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions; const { width, height } = state.canvas.boundingBoxDimensions;
const model = modelIdToMainModelField(currentModel?.id || ''); if (!model) {
moduleLog.error('No model found in state');
throw new Error('No model found in state');
}
const use_cpu = shouldUseNoiseSettings const use_cpu = shouldUseNoiseSettings
? shouldUseCpuNoise ? shouldUseCpuNoise
@ -180,16 +186,49 @@ export const buildCanvasTextToImageGraph = (
], ],
}; };
addLoRAsToGraph(graph, state, TEXT_TO_LATENTS); // add metadata accumulator, which is only mostly populated - some fields are added later
graph.nodes[METADATA_ACCUMULATOR] = {
id: METADATA_ACCUMULATOR,
type: 'metadata_accumulator',
generation_mode: 'txt2img',
cfg_scale,
height,
width,
positive_prompt: '', // set in addDynamicPromptsToGraph
negative_prompt: negativePrompt,
model,
seed: 0, // set in addDynamicPromptsToGraph
steps,
rand_device: use_cpu ? 'cpu' : 'cuda',
scheduler,
vae: undefined, // option; set in addVAEToGraph
controlnets: [], // populated in addControlNetToLinearGraph
loras: [], // populated in addLoRAsToGraph
clip_skip: clipSkip,
};
// Add VAE graph.edges.push({
addVAEToGraph(graph, state); source: {
node_id: METADATA_ACCUMULATOR,
field: 'metadata',
},
destination: {
node_id: LATENTS_TO_IMAGE,
field: 'metadata',
},
});
// add dynamic prompts, mutating `graph` // add LoRA support
addDynamicPromptsToGraph(graph, state); addLoRAsToGraph(state, graph, TEXT_TO_LATENTS);
// optionally add custom VAE
addVAEToGraph(state, graph);
// add dynamic prompts - also sets up core iteration and seed
addDynamicPromptsToGraph(state, graph);
// add controlnet, mutating `graph` // add controlnet, mutating `graph`
addControlNetToLinearGraph(graph, TEXT_TO_LATENTS, state); addControlNetToLinearGraph(state, graph, TEXT_TO_LATENTS);
return graph; return graph;
}; };

View File

@ -3,25 +3,21 @@ import { RootState } from 'app/store/store';
import { NonNullableGraph } from 'features/nodes/types/types'; import { NonNullableGraph } from 'features/nodes/types/types';
import { initialGenerationState } from 'features/parameters/store/generationSlice'; import { initialGenerationState } from 'features/parameters/store/generationSlice';
import { import {
ImageCollectionInvocation,
ImageResizeInvocation, ImageResizeInvocation,
ImageToLatentsInvocation, ImageToLatentsInvocation,
IterateInvocation,
} from 'services/api/types'; } from 'services/api/types';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
import { modelIdToMainModelField } from '../modelIdToMainModelField';
import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph'; import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph';
import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addLoRAsToGraph } from './addLoRAsToGraph';
import { addVAEToGraph } from './addVAEToGraph'; import { addVAEToGraph } from './addVAEToGraph';
import { import {
CLIP_SKIP, CLIP_SKIP,
IMAGE_COLLECTION,
IMAGE_COLLECTION_ITERATE,
IMAGE_TO_IMAGE_GRAPH, IMAGE_TO_IMAGE_GRAPH,
IMAGE_TO_LATENTS, IMAGE_TO_LATENTS,
LATENTS_TO_IMAGE, LATENTS_TO_IMAGE,
LATENTS_TO_LATENTS, LATENTS_TO_LATENTS,
MAIN_MODEL_LOADER, MAIN_MODEL_LOADER,
METADATA_ACCUMULATOR,
NEGATIVE_CONDITIONING, NEGATIVE_CONDITIONING,
NOISE, NOISE,
POSITIVE_CONDITIONING, POSITIVE_CONDITIONING,
@ -39,7 +35,7 @@ export const buildLinearImageToImageGraph = (
const { const {
positivePrompt, positivePrompt,
negativePrompt, negativePrompt,
model: currentModel, model,
cfgScale: cfg_scale, cfgScale: cfg_scale,
scheduler, scheduler,
steps, steps,
@ -53,14 +49,15 @@ export const buildLinearImageToImageGraph = (
shouldUseNoiseSettings, shouldUseNoiseSettings,
} = state.generation; } = state.generation;
const { // TODO: add batch functionality
isEnabled: isBatchEnabled, // const {
imageNames: batchImageNames, // isEnabled: isBatchEnabled,
asInitialImage, // imageNames: batchImageNames,
} = state.batch; // asInitialImage,
// } = state.batch;
const shouldBatch = // const shouldBatch =
isBatchEnabled && batchImageNames.length > 0 && asInitialImage; // isBatchEnabled && batchImageNames.length > 0 && asInitialImage;
/** /**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
@ -71,12 +68,15 @@ export const buildLinearImageToImageGraph = (
* the `fit` param. These are added to the graph at the end. * the `fit` param. These are added to the graph at the end.
*/ */
if (!initialImage && !shouldBatch) { if (!initialImage) {
moduleLog.error('No initial image found in state'); moduleLog.error('No initial image found in state');
throw new Error('No initial image found in state'); throw new Error('No initial image found in state');
} }
const model = modelIdToMainModelField(currentModel?.id || ''); if (!model) {
moduleLog.error('No model found in state');
throw new Error('No model found in state');
}
const use_cpu = shouldUseNoiseSettings const use_cpu = shouldUseNoiseSettings
? shouldUseCpuNoise ? shouldUseCpuNoise
@ -295,51 +295,87 @@ export const buildLinearImageToImageGraph = (
}); });
} }
if (isBatchEnabled && asInitialImage && batchImageNames.length > 0) { // TODO: add batch functionality
// we are going to connect an iterate up to the init image // if (isBatchEnabled && asInitialImage && batchImageNames.length > 0) {
delete (graph.nodes[IMAGE_TO_LATENTS] as ImageToLatentsInvocation).image; // // we are going to connect an iterate up to the init image
// delete (graph.nodes[IMAGE_TO_LATENTS] as ImageToLatentsInvocation).image;
const imageCollection: ImageCollectionInvocation = { // const imageCollection: ImageCollectionInvocation = {
id: IMAGE_COLLECTION, // id: IMAGE_COLLECTION,
type: 'image_collection', // type: 'image_collection',
images: batchImageNames.map((image_name) => ({ image_name })), // images: batchImageNames.map((image_name) => ({ image_name })),
}; // };
const imageCollectionIterate: IterateInvocation = { // const imageCollectionIterate: IterateInvocation = {
id: IMAGE_COLLECTION_ITERATE, // id: IMAGE_COLLECTION_ITERATE,
type: 'iterate', // type: 'iterate',
}; // };
graph.nodes[IMAGE_COLLECTION] = imageCollection; // graph.nodes[IMAGE_COLLECTION] = imageCollection;
graph.nodes[IMAGE_COLLECTION_ITERATE] = imageCollectionIterate; // graph.nodes[IMAGE_COLLECTION_ITERATE] = imageCollectionIterate;
graph.edges.push({ // graph.edges.push({
source: { node_id: IMAGE_COLLECTION, field: 'collection' }, // source: { node_id: IMAGE_COLLECTION, field: 'collection' },
destination: { // destination: {
node_id: IMAGE_COLLECTION_ITERATE, // node_id: IMAGE_COLLECTION_ITERATE,
field: 'collection', // field: 'collection',
}, // },
}); // });
graph.edges.push({ // graph.edges.push({
source: { node_id: IMAGE_COLLECTION_ITERATE, field: 'item' }, // source: { node_id: IMAGE_COLLECTION_ITERATE, field: 'item' },
destination: { // destination: {
node_id: IMAGE_TO_LATENTS, // node_id: IMAGE_TO_LATENTS,
field: 'image', // field: 'image',
}, // },
}); // });
} // }
addLoRAsToGraph(graph, state, LATENTS_TO_LATENTS); // add metadata accumulator, which is only mostly populated - some fields are added later
graph.nodes[METADATA_ACCUMULATOR] = {
id: METADATA_ACCUMULATOR,
type: 'metadata_accumulator',
generation_mode: 'img2img',
cfg_scale,
height,
width,
positive_prompt: '', // set in addDynamicPromptsToGraph
negative_prompt: negativePrompt,
model,
seed: 0, // set in addDynamicPromptsToGraph
steps,
rand_device: use_cpu ? 'cpu' : 'cuda',
scheduler,
vae: undefined, // option; set in addVAEToGraph
controlnets: [], // populated in addControlNetToLinearGraph
loras: [], // populated in addLoRAsToGraph
clip_skip: clipSkip,
strength,
init_image: initialImage.imageName,
};
// Add VAE graph.edges.push({
addVAEToGraph(graph, state); source: {
node_id: METADATA_ACCUMULATOR,
field: 'metadata',
},
destination: {
node_id: LATENTS_TO_IMAGE,
field: 'metadata',
},
});
// add dynamic prompts, mutating `graph` // add LoRA support
addDynamicPromptsToGraph(graph, state); addLoRAsToGraph(state, graph, LATENTS_TO_LATENTS);
// optionally add custom VAE
addVAEToGraph(state, graph);
// add dynamic prompts - also sets up core iteration and seed
addDynamicPromptsToGraph(state, graph);
// add controlnet, mutating `graph` // add controlnet, mutating `graph`
addControlNetToLinearGraph(graph, LATENTS_TO_LATENTS, state); addControlNetToLinearGraph(state, graph, LATENTS_TO_LATENTS);
return graph; return graph;
}; };

View File

@ -1,8 +1,8 @@
import { log } from 'app/logging/useLogger';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { NonNullableGraph } from 'features/nodes/types/types'; import { NonNullableGraph } from 'features/nodes/types/types';
import { initialGenerationState } from 'features/parameters/store/generationSlice'; import { initialGenerationState } from 'features/parameters/store/generationSlice';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
import { modelIdToMainModelField } from '../modelIdToMainModelField';
import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph'; import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph';
import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addLoRAsToGraph } from './addLoRAsToGraph';
import { addVAEToGraph } from './addVAEToGraph'; import { addVAEToGraph } from './addVAEToGraph';
@ -10,6 +10,7 @@ import {
CLIP_SKIP, CLIP_SKIP,
LATENTS_TO_IMAGE, LATENTS_TO_IMAGE,
MAIN_MODEL_LOADER, MAIN_MODEL_LOADER,
METADATA_ACCUMULATOR,
NEGATIVE_CONDITIONING, NEGATIVE_CONDITIONING,
NOISE, NOISE,
POSITIVE_CONDITIONING, POSITIVE_CONDITIONING,
@ -17,13 +18,15 @@ import {
TEXT_TO_LATENTS, TEXT_TO_LATENTS,
} from './constants'; } from './constants';
const moduleLog = log.child({ namespace: 'nodes' });
export const buildLinearTextToImageGraph = ( export const buildLinearTextToImageGraph = (
state: RootState state: RootState
): NonNullableGraph => { ): NonNullableGraph => {
const { const {
positivePrompt, positivePrompt,
negativePrompt, negativePrompt,
model: currentModel, model,
cfgScale: cfg_scale, cfgScale: cfg_scale,
scheduler, scheduler,
steps, steps,
@ -34,12 +37,15 @@ export const buildLinearTextToImageGraph = (
shouldUseNoiseSettings, shouldUseNoiseSettings,
} = state.generation; } = state.generation;
const model = modelIdToMainModelField(currentModel?.id || '');
const use_cpu = shouldUseNoiseSettings const use_cpu = shouldUseNoiseSettings
? shouldUseCpuNoise ? shouldUseCpuNoise
: initialGenerationState.shouldUseCpuNoise; : initialGenerationState.shouldUseCpuNoise;
if (!model) {
moduleLog.error('No model found in state');
throw new Error('No model found in state');
}
/** /**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
* full graph here as a template. Then use the parameters from app state and set friendlier node * full graph here as a template. Then use the parameters from app state and set friendlier node
@ -176,16 +182,49 @@ export const buildLinearTextToImageGraph = (
], ],
}; };
addLoRAsToGraph(graph, state, TEXT_TO_LATENTS); // add metadata accumulator, which is only mostly populated - some fields are added later
graph.nodes[METADATA_ACCUMULATOR] = {
id: METADATA_ACCUMULATOR,
type: 'metadata_accumulator',
generation_mode: 'txt2img',
cfg_scale,
height,
width,
positive_prompt: '', // set in addDynamicPromptsToGraph
negative_prompt: negativePrompt,
model,
seed: 0, // set in addDynamicPromptsToGraph
steps,
rand_device: use_cpu ? 'cpu' : 'cuda',
scheduler,
vae: undefined, // option; set in addVAEToGraph
controlnets: [], // populated in addControlNetToLinearGraph
loras: [], // populated in addLoRAsToGraph
clip_skip: clipSkip,
};
// Add Custom VAE Support graph.edges.push({
addVAEToGraph(graph, state); source: {
node_id: METADATA_ACCUMULATOR,
field: 'metadata',
},
destination: {
node_id: LATENTS_TO_IMAGE,
field: 'metadata',
},
});
// add dynamic prompts, mutating `graph` // add LoRA support
addDynamicPromptsToGraph(graph, state); addLoRAsToGraph(state, graph, TEXT_TO_LATENTS);
// optionally add custom VAE
addVAEToGraph(state, graph);
// add dynamic prompts - also sets up core iteration and seed
addDynamicPromptsToGraph(state, graph);
// add controlnet, mutating `graph` // add controlnet, mutating `graph`
addControlNetToLinearGraph(graph, TEXT_TO_LATENTS, state); addControlNetToLinearGraph(state, graph, TEXT_TO_LATENTS);
return graph; return graph;
}; };

View File

@ -19,6 +19,7 @@ export const CONTROL_NET_COLLECT = 'control_net_collect';
export const DYNAMIC_PROMPT = 'dynamic_prompt'; export const DYNAMIC_PROMPT = 'dynamic_prompt';
export const IMAGE_COLLECTION = 'image_collection'; export const IMAGE_COLLECTION = 'image_collection';
export const IMAGE_COLLECTION_ITERATE = 'image_collection_iterate'; export const IMAGE_COLLECTION_ITERATE = 'image_collection_iterate';
export const METADATA_ACCUMULATOR = 'metadata_accumulator';
// 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';

View File

@ -5,17 +5,21 @@ import {
InputFieldTemplate, InputFieldTemplate,
InvocationSchemaObject, InvocationSchemaObject,
InvocationTemplate, InvocationTemplate,
isInvocationSchemaObject,
OutputFieldTemplate, OutputFieldTemplate,
isInvocationSchemaObject,
} from '../types/types'; } from '../types/types';
import { import {
buildInputFieldTemplate, buildInputFieldTemplate,
buildOutputFieldTemplates, buildOutputFieldTemplates,
} from './fieldTemplateBuilders'; } from './fieldTemplateBuilders';
const RESERVED_FIELD_NAMES = ['id', 'type', 'is_intermediate']; const RESERVED_FIELD_NAMES = ['id', 'type', 'is_intermediate', 'core_metadata'];
const invocationDenylist = ['Graph', 'InvocationMeta']; const invocationDenylist = [
'Graph',
'InvocationMeta',
'MetadataAccumulatorInvocation',
];
export const parseSchema = (openAPI: OpenAPIV3.Document) => { export const parseSchema = (openAPI: OpenAPIV3.Document) => {
// filter out non-invocation schemas, plus some tricky invocations for now // filter out non-invocation schemas, plus some tricky invocations for now

View File

@ -2,6 +2,7 @@ import { useAppToaster } from 'app/components/Toaster';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { UnsafeImageMetadata } from 'services/api/endpoints/images';
import { isImageField } from 'services/api/guards'; import { isImageField } from 'services/api/guards';
import { ImageDTO } from 'services/api/types'; import { ImageDTO } from 'services/api/types';
import { initialImageSelected, modelSelected } from '../store/actions'; import { initialImageSelected, modelSelected } from '../store/actions';
@ -162,7 +163,7 @@ export const useRecallParameters = () => {
parameterNotSetToast(); parameterNotSetToast();
return; return;
} }
dispatch(modelSelected(model?.id || '')); dispatch(modelSelected(model));
parameterSetToast(); parameterSetToast();
}, },
[dispatch, parameterSetToast, parameterNotSetToast] [dispatch, parameterSetToast, parameterNotSetToast]
@ -269,28 +270,24 @@ export const useRecallParameters = () => {
); );
const recallAllParameters = useCallback( const recallAllParameters = useCallback(
(image: ImageDTO | undefined) => { (metadata: UnsafeImageMetadata['metadata'] | undefined) => {
if (!image || !image.metadata) { if (!metadata) {
allParameterNotSetToast(); allParameterNotSetToast();
return; return;
} }
const { const {
cfg_scale, cfg_scale,
height, height,
model, model,
positive_conditioning, positive_prompt,
negative_conditioning, negative_prompt,
scheduler, scheduler,
seed, seed,
steps, steps,
width, width,
strength, strength,
clip, } = metadata;
extra,
latents,
unet,
vae,
} = image.metadata;
if (isValidCfgScale(cfg_scale)) { if (isValidCfgScale(cfg_scale)) {
dispatch(setCfgScale(cfg_scale)); dispatch(setCfgScale(cfg_scale));
@ -298,11 +295,11 @@ export const useRecallParameters = () => {
if (isValidMainModel(model)) { if (isValidMainModel(model)) {
dispatch(modelSelected(model)); dispatch(modelSelected(model));
} }
if (isValidPositivePrompt(positive_conditioning)) { if (isValidPositivePrompt(positive_prompt)) {
dispatch(setPositivePrompt(positive_conditioning)); dispatch(setPositivePrompt(positive_prompt));
} }
if (isValidNegativePrompt(negative_conditioning)) { if (isValidNegativePrompt(negative_prompt)) {
dispatch(setNegativePrompt(negative_conditioning)); dispatch(setNegativePrompt(negative_prompt));
} }
if (isValidScheduler(scheduler)) { if (isValidScheduler(scheduler)) {
dispatch(setScheduler(scheduler)); dispatch(setScheduler(scheduler));

View File

@ -1,8 +1,10 @@
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import { ImageDTO } from 'services/api/types'; import { ImageDTO, MainModelField } from 'services/api/types';
export const initialImageSelected = createAction<ImageDTO | string | undefined>( export const initialImageSelected = createAction<ImageDTO | string | undefined>(
'generation/initialImageSelected' 'generation/initialImageSelected'
); );
export const modelSelected = createAction<string>('generation/modelSelected'); export const modelSelected = createAction<MainModelField>(
'generation/modelSelected'
);

View File

@ -8,12 +8,11 @@ import {
setShouldShowAdvancedOptions, setShouldShowAdvancedOptions,
} from 'features/ui/store/uiSlice'; } from 'features/ui/store/uiSlice';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { ImageDTO } from 'services/api/types'; import { ImageDTO, MainModelField } from 'services/api/types';
import { clipSkipMap } from '../components/Parameters/Advanced/ParamClipSkip'; import { clipSkipMap } from '../components/Parameters/Advanced/ParamClipSkip';
import { import {
CfgScaleParam, CfgScaleParam,
HeightParam, HeightParam,
MainModelParam,
NegativePromptParam, NegativePromptParam,
PositivePromptParam, PositivePromptParam,
SchedulerParam, SchedulerParam,
@ -54,7 +53,7 @@ export interface GenerationState {
shouldUseSymmetry: boolean; shouldUseSymmetry: boolean;
horizontalSymmetrySteps: number; horizontalSymmetrySteps: number;
verticalSymmetrySteps: number; verticalSymmetrySteps: number;
model: MainModelParam | null; model: MainModelField | null;
vae: VaeModelParam | null; vae: VaeModelParam | null;
seamlessXAxis: boolean; seamlessXAxis: boolean;
seamlessYAxis: boolean; seamlessYAxis: boolean;
@ -227,23 +226,17 @@ export const generationSlice = createSlice({
const { image_name, width, height } = action.payload; const { image_name, width, height } = action.payload;
state.initialImage = { imageName: image_name, width, height }; state.initialImage = { imageName: image_name, width, height };
}, },
modelSelected: (state, action: PayloadAction<string>) => { modelChanged: (state, action: PayloadAction<MainModelField | null>) => {
const [base_model, type, name] = action.payload.split('/'); if (!action.payload) {
state.model = null;
}
state.model = zMainModel.parse({ state.model = zMainModel.parse(action.payload);
id: action.payload,
base_model,
name,
type,
});
// Clamp ClipSkip Based On Selected Model // Clamp ClipSkip Based On Selected Model
const { maxClip } = clipSkipMap[state.model.base_model]; const { maxClip } = clipSkipMap[state.model.base_model];
state.clipSkip = clamp(state.clipSkip, 0, maxClip); state.clipSkip = clamp(state.clipSkip, 0, maxClip);
}, },
modelChanged: (state, action: PayloadAction<MainModelParam>) => {
state.model = action.payload;
},
vaeSelected: (state, action: PayloadAction<VaeModelParam | null>) => { vaeSelected: (state, action: PayloadAction<VaeModelParam | null>) => {
state.vae = action.payload; state.vae = action.payload;
}, },

View File

@ -135,8 +135,7 @@ export type BaseModelParam = z.infer<typeof zBaseModel>;
* TODO: Make this a dynamically generated enum? * TODO: Make this a dynamically generated enum?
*/ */
export const zMainModel = z.object({ export const zMainModel = z.object({
id: z.string(), model_name: z.string(),
name: z.string(),
base_model: zBaseModel, base_model: zBaseModel,
}); });

View File

@ -1,13 +1,16 @@
import { memo, useCallback, useEffect, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { SelectItem } from '@mantine/core'; import { SelectItem } from '@mantine/core';
import { RootState } from 'app/store/store'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { modelIdToMainModelField } from 'features/nodes/util/modelIdToMainModelField';
import { modelSelected } from 'features/parameters/store/actions'; import { modelSelected } from 'features/parameters/store/actions';
import { forEach, isString } from 'lodash-es'; import { forEach } from 'lodash-es';
import { useGetMainModelsQuery } from 'services/api/endpoints/models'; import { useGetMainModelsQuery } from 'services/api/endpoints/models';
export const MODEL_TYPE_MAP = { export const MODEL_TYPE_MAP = {
@ -15,13 +18,17 @@ export const MODEL_TYPE_MAP = {
'sd-2': 'Stable Diffusion 2.x', 'sd-2': 'Stable Diffusion 2.x',
}; };
const selector = createSelector(
stateSelector,
(state) => ({ currentModel: state.generation.model }),
defaultSelectorOptions
);
const ModelSelect = () => { const ModelSelect = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const currentModel = useAppSelector( const { currentModel } = useAppSelector(selector);
(state: RootState) => state.generation.model
);
const { data: mainModels, isLoading } = useGetMainModelsQuery(); const { data: mainModels, isLoading } = useGetMainModelsQuery();
@ -39,7 +46,7 @@ const ModelSelect = () => {
data.push({ data.push({
value: id, value: id,
label: model.name, label: model.model_name,
group: MODEL_TYPE_MAP[model.base_model], group: MODEL_TYPE_MAP[model.base_model],
}); });
}); });
@ -48,7 +55,10 @@ const ModelSelect = () => {
}, [mainModels]); }, [mainModels]);
const selectedModel = useMemo( const selectedModel = useMemo(
() => mainModels?.entities[currentModel?.id || ''], () =>
mainModels?.entities[
`${currentModel?.base_model}/main/${currentModel?.model_name}`
],
[mainModels?.entities, currentModel] [mainModels?.entities, currentModel]
); );
@ -57,31 +67,13 @@ const ModelSelect = () => {
if (!v) { if (!v) {
return; return;
} }
dispatch(modelSelected(v));
const modelField = modelIdToMainModelField(v);
dispatch(modelSelected(modelField));
}, },
[dispatch] [dispatch]
); );
useEffect(() => {
if (isLoading) {
// return early here to avoid resetting model selection before we've loaded the available models
return;
}
if (selectedModel && mainModels?.ids.includes(selectedModel?.id)) {
// the selected model is an available model, no need to change it
return;
}
const firstModel = mainModels?.ids[0];
if (!isString(firstModel)) {
return;
}
handleChangeModel(firstModel);
}, [handleChangeModel, isLoading, mainModels?.ids, selectedModel]);
return isLoading ? ( return isLoading ? (
<IAIMantineSelect <IAIMantineSelect
label={t('modelManager.model')} label={t('modelManager.model')}
@ -94,9 +86,10 @@ const ModelSelect = () => {
tooltip={selectedModel?.description} tooltip={selectedModel?.description}
label={t('modelManager.model')} label={t('modelManager.model')}
value={selectedModel?.id} value={selectedModel?.id}
placeholder={data.length > 0 ? 'Select a model' : 'No models detected!'} placeholder={data.length > 0 ? 'Select a model' : 'No models available'}
data={data} data={data}
error={data.length === 0} error={data.length === 0}
disabled={data.length === 0}
onChange={handleChangeModel} onChange={handleChangeModel}
/> />
); );

View File

@ -50,7 +50,7 @@ const VAESelect = () => {
data.push({ data.push({
value: id, value: id,
label: model.name, label: model.model_name,
group: MODEL_TYPE_MAP[model.base_model], group: MODEL_TYPE_MAP[model.base_model],
disabled, disabled,
tooltip: disabled tooltip: disabled

View File

@ -1,13 +1,22 @@
import { ApiFullTagDescription, api } from '..'; import { ApiFullTagDescription, api } from '..';
import { components } from '../schema';
import { ImageDTO } from '../types'; import { ImageDTO } from '../types';
/**
* This is an unsafe type; the object inside is not guaranteed to be valid.
*/
export type UnsafeImageMetadata = {
metadata: components['schemas']['CoreMetadata'];
graph: NonNullable<components['schemas']['Graph']>;
};
export const imagesApi = api.injectEndpoints({ export const imagesApi = api.injectEndpoints({
endpoints: (build) => ({ endpoints: (build) => ({
/** /**
* Image Queries * Image Queries
*/ */
getImageDTO: build.query<ImageDTO, string>({ getImageDTO: build.query<ImageDTO, string>({
query: (image_name) => ({ url: `images/${image_name}/metadata` }), query: (image_name) => ({ url: `images/${image_name}` }),
providesTags: (result, error, arg) => { providesTags: (result, error, arg) => {
const tags: ApiFullTagDescription[] = [{ type: 'Image', id: arg }]; const tags: ApiFullTagDescription[] = [{ type: 'Image', id: arg }];
if (result?.board_id) { if (result?.board_id) {
@ -17,7 +26,17 @@ export const imagesApi = api.injectEndpoints({
}, },
keepUnusedDataFor: 86400, // 24 hours keepUnusedDataFor: 86400, // 24 hours
}), }),
getImageMetadata: build.query<UnsafeImageMetadata, string>({
query: (image_name) => ({ url: `images/${image_name}/metadata` }),
providesTags: (result, error, arg) => {
const tags: ApiFullTagDescription[] = [
{ type: 'ImageMetadata', id: arg },
];
return tags;
},
keepUnusedDataFor: 86400, // 24 hours
}),
}), }),
}); });
export const { useGetImageDTOQuery } = imagesApi; export const { useGetImageDTOQuery, useGetImageMetadataQuery } = imagesApi;

View File

@ -33,25 +33,28 @@ type AnyModelConfigEntity =
| VaeModelConfigEntity; | VaeModelConfigEntity;
const mainModelsAdapter = createEntityAdapter<MainModelConfigEntity>({ const mainModelsAdapter = createEntityAdapter<MainModelConfigEntity>({
sortComparer: (a, b) => a.name.localeCompare(b.name), sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
}); });
const loraModelsAdapter = createEntityAdapter<LoRAModelConfigEntity>({ const loraModelsAdapter = createEntityAdapter<LoRAModelConfigEntity>({
sortComparer: (a, b) => a.name.localeCompare(b.name), sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
}); });
const controlNetModelsAdapter = const controlNetModelsAdapter =
createEntityAdapter<ControlNetModelConfigEntity>({ createEntityAdapter<ControlNetModelConfigEntity>({
sortComparer: (a, b) => a.name.localeCompare(b.name), sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
}); });
const textualInversionModelsAdapter = const textualInversionModelsAdapter =
createEntityAdapter<TextualInversionModelConfigEntity>({ createEntityAdapter<TextualInversionModelConfigEntity>({
sortComparer: (a, b) => a.name.localeCompare(b.name), sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
}); });
const vaeModelsAdapter = createEntityAdapter<VaeModelConfigEntity>({ const vaeModelsAdapter = createEntityAdapter<VaeModelConfigEntity>({
sortComparer: (a, b) => a.name.localeCompare(b.name), sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
}); });
export const getModelId = ({ base_model, type, name }: AnyModelConfig) => export const getModelId = ({
`${base_model}/${type}/${name}`; base_model,
model_type,
model_name,
}: AnyModelConfig) => `${base_model}/${model_type}/${model_name}`;
const createModelEntities = <T extends AnyModelConfigEntity>( const createModelEntities = <T extends AnyModelConfigEntity>(
models: AnyModelConfig[] models: AnyModelConfig[]

View File

@ -1,3 +1,4 @@
import { FullTagDescription } from '@reduxjs/toolkit/dist/query/endpointDefinitions';
import { import {
BaseQueryFn, BaseQueryFn,
FetchArgs, FetchArgs,
@ -5,10 +6,9 @@ import {
createApi, createApi,
fetchBaseQuery, fetchBaseQuery,
} from '@reduxjs/toolkit/query/react'; } from '@reduxjs/toolkit/query/react';
import { FullTagDescription } from '@reduxjs/toolkit/dist/query/endpointDefinitions';
import { $authToken, $baseUrl } from 'services/api/client'; import { $authToken, $baseUrl } from 'services/api/client';
export const tagTypes = ['Board', 'Image', 'Model']; export const tagTypes = ['Board', 'Image', 'ImageMetadata', 'Model'];
export type ApiFullTagDescription = FullTagDescription< export type ApiFullTagDescription = FullTagDescription<
(typeof tagTypes)[number] (typeof tagTypes)[number]
>; >;

View File

@ -109,10 +109,10 @@ export type paths = {
}; };
"/api/v1/images/": { "/api/v1/images/": {
/** /**
* List Images With Metadata * List Image Dtos
* @description Gets a list of images * @description Gets a list of image DTOs
*/ */
get: operations["list_images_with_metadata"]; get: operations["list_image_dtos"];
/** /**
* Upload Image * Upload Image
* @description Uploads an image * @description Uploads an image
@ -121,10 +121,10 @@ export type paths = {
}; };
"/api/v1/images/{image_name}": { "/api/v1/images/{image_name}": {
/** /**
* Get Image Full * Get Image Dto
* @description Gets a full-resolution image file * @description Gets an image's DTO
*/ */
get: operations["get_image_full"]; get: operations["get_image_dto"];
/** /**
* Delete Image * Delete Image
* @description Deletes an image * @description Deletes an image
@ -143,6 +143,13 @@ export type paths = {
*/ */
get: operations["get_image_metadata"]; get: operations["get_image_metadata"];
}; };
"/api/v1/images/{image_name}/full": {
/**
* Get Image Full
* @description Gets a full-resolution image file
*/
get: operations["get_image_full"];
};
"/api/v1/images/{image_name}/thumbnail": { "/api/v1/images/{image_name}/thumbnail": {
/** /**
* Get Image Thumbnail * Get Image Thumbnail
@ -798,14 +805,14 @@ export type components = {
}; };
/** ControlNetModelConfig */ /** ControlNetModelConfig */
ControlNetModelConfig: { ControlNetModelConfig: {
/** Name */ /** Model Name */
name: string; model_name: string;
base_model: components["schemas"]["BaseModelType"]; base_model: components["schemas"]["BaseModelType"];
/** /**
* Type * Model Type
* @enum {string} * @enum {string}
*/ */
type: "controlnet"; model_type: "controlnet";
/** Path */ /** Path */
path: string; path: string;
/** Description */ /** Description */
@ -836,6 +843,97 @@ export type components = {
*/ */
control?: components["schemas"]["ControlField"]; control?: components["schemas"]["ControlField"];
}; };
/**
* CoreMetadata
* @description Core generation metadata for an image generated in InvokeAI.
*/
CoreMetadata: {
/**
* Generation Mode
* @description The generation mode that output this image
*/
generation_mode: string;
/**
* Positive Prompt
* @description The positive prompt parameter
*/
positive_prompt: string;
/**
* Negative Prompt
* @description The negative prompt parameter
*/
negative_prompt: string;
/**
* Width
* @description The width parameter
*/
width: number;
/**
* Height
* @description The height parameter
*/
height: number;
/**
* Seed
* @description The seed used for noise generation
*/
seed: number;
/**
* Rand Device
* @description The device used for random number generation
*/
rand_device: string;
/**
* Cfg Scale
* @description The classifier-free guidance scale parameter
*/
cfg_scale: number;
/**
* Steps
* @description The number of steps used for inference
*/
steps: number;
/**
* Scheduler
* @description The scheduler used for inference
*/
scheduler: string;
/**
* Clip Skip
* @description The number of skipped CLIP layers
*/
clip_skip: number;
/**
* Model
* @description The main model used for inference
*/
model: components["schemas"]["MainModelField"];
/**
* Controlnets
* @description The ControlNets used for inference
*/
controlnets: (components["schemas"]["ControlField"])[];
/**
* Loras
* @description The LoRAs used for inference
*/
loras: (components["schemas"]["LoRAMetadataField"])[];
/**
* Strength
* @description The strength used for latents-to-latents
*/
strength?: number;
/**
* Init Image
* @description The name of the initial image
*/
init_image?: string;
/**
* Vae
* @description The VAE used for decoding, if the main model's default was not used
*/
vae?: components["schemas"]["VAEModelField"];
};
/** /**
* CvInpaintInvocation * CvInpaintInvocation
* @description Simple inpaint using opencv. * @description Simple inpaint using opencv.
@ -1058,7 +1156,7 @@ export type components = {
* @description The nodes in this graph * @description The nodes in this graph
*/ */
nodes?: { nodes?: {
[key: string]: (components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]) | undefined; [key: string]: (components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]) | undefined;
}; };
/** /**
* Edges * Edges
@ -1101,7 +1199,7 @@ export type components = {
* @description The results of node executions * @description The results of node executions
*/ */
results: { results: {
[key: string]: (components["schemas"]["IntCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["CompelOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["IntOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PromptOutput"] | components["schemas"]["PromptCollectionOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]) | undefined; [key: string]: (components["schemas"]["ImageOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["MetadataAccumulatorOutput"] | components["schemas"]["PromptOutput"] | components["schemas"]["PromptCollectionOutput"] | components["schemas"]["CompelOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["IntOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["IntCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]) | undefined;
}; };
/** /**
* Errors * Errors
@ -1502,11 +1600,6 @@ export type components = {
* @description The node ID that generated this image, if it is a generated image. * @description The node ID that generated this image, if it is a generated image.
*/ */
node_id?: string; node_id?: string;
/**
* Metadata
* @description A limited subset of the image's generation metadata. Retrieve the image's session for full metadata.
*/
metadata?: components["schemas"]["ImageMetadata"];
/** /**
* Board Id * Board Id
* @description The id of the board the image belongs to, if one exists. * @description The id of the board the image belongs to, if one exists.
@ -1606,96 +1699,19 @@ export type components = {
}; };
/** /**
* ImageMetadata * ImageMetadata
* @description Core generation metadata for an image/tensor generated in InvokeAI. * @description An image's generation metadata
*
* Also includes any metadata from the image's PNG tEXt chunks.
*
* Generated by traversing the execution graph, collecting the parameters of the nearest ancestors
* of a given node.
*
* Full metadata may be accessed by querying for the session in the `graph_executions` table.
*/ */
ImageMetadata: { ImageMetadata: {
/** /**
* Type * Metadata
* @description The type of the ancestor node of the image output node. * @description The image's core metadata, if it was created in the Linear or Canvas UI
*/ */
type?: string; metadata?: Record<string, never>;
/** /**
* Positive Conditioning * Graph
* @description The positive conditioning. * @description The graph that created the image
*/ */
positive_conditioning?: string; graph?: Record<string, never>;
/**
* Negative Conditioning
* @description The negative conditioning.
*/
negative_conditioning?: string;
/**
* Width
* @description Width of the image/latents in pixels.
*/
width?: number;
/**
* Height
* @description Height of the image/latents in pixels.
*/
height?: number;
/**
* Seed
* @description The seed used for noise generation.
*/
seed?: number;
/**
* Cfg Scale
* @description The classifier-free guidance scale.
*/
cfg_scale?: number | (number)[];
/**
* Steps
* @description The number of steps used for inference.
*/
steps?: number;
/**
* Scheduler
* @description The scheduler used for inference.
*/
scheduler?: string;
/**
* Model
* @description The model used for inference.
*/
model?: string;
/**
* Strength
* @description The strength used for image-to-image/latents-to-latents.
*/
strength?: number;
/**
* Latents
* @description The ID of the initial latents.
*/
latents?: string;
/**
* Vae
* @description The VAE used for decoding.
*/
vae?: string;
/**
* Unet
* @description The UNet used dor inference.
*/
unet?: string;
/**
* Clip
* @description The CLIP Encoder used for conditioning.
*/
clip?: string;
/**
* Extra
* @description Uploaded image metadata, extracted from the PNG tEXt chunk.
*/
extra?: string;
}; };
/** /**
* ImageMultiplyInvocation * ImageMultiplyInvocation
@ -2436,6 +2452,11 @@ export type components = {
* @default false * @default false
*/ */
tiled?: boolean; tiled?: boolean;
/**
* Metadata
* @description Optional core metadata to be written to the image
*/
metadata?: components["schemas"]["CoreMetadata"];
}; };
/** /**
* LatentsToLatentsInvocation * LatentsToLatentsInvocation
@ -2659,16 +2680,32 @@ export type components = {
*/ */
coarse?: boolean; coarse?: boolean;
}; };
/**
* LoRAMetadataField
* @description LoRA metadata for an image generated in InvokeAI.
*/
LoRAMetadataField: {
/**
* Lora
* @description The LoRA model
*/
lora: components["schemas"]["LoRAModelField"];
/**
* Weight
* @description The weight of the LoRA model
*/
weight: number;
};
/** LoRAModelConfig */ /** LoRAModelConfig */
LoRAModelConfig: { LoRAModelConfig: {
/** Name */ /** Model Name */
name: string; model_name: string;
base_model: components["schemas"]["BaseModelType"]; base_model: components["schemas"]["BaseModelType"];
/** /**
* Type * Model Type
* @enum {string} * @enum {string}
*/ */
type: "lora"; model_type: "lora";
/** Path */ /** Path */
path: string; path: string;
/** Description */ /** Description */
@ -2956,6 +2993,131 @@ export type components = {
* @enum {string} * @enum {string}
*/ */
MergeInterpolationMethod: "weighted_sum" | "sigmoid" | "inv_sigmoid" | "add_difference"; MergeInterpolationMethod: "weighted_sum" | "sigmoid" | "inv_sigmoid" | "add_difference";
/**
* MetadataAccumulatorInvocation
* @description Outputs a Core Metadata Object
*/
MetadataAccumulatorInvocation: {
/**
* Id
* @description The id of this node. Must be unique among all nodes.
*/
id: string;
/**
* Is Intermediate
* @description Whether or not this node is an intermediate node.
* @default false
*/
is_intermediate?: boolean;
/**
* Type
* @default metadata_accumulator
* @enum {string}
*/
type?: "metadata_accumulator";
/**
* Generation Mode
* @description The generation mode that output this image
*/
generation_mode: string;
/**
* Positive Prompt
* @description The positive prompt parameter
*/
positive_prompt: string;
/**
* Negative Prompt
* @description The negative prompt parameter
*/
negative_prompt: string;
/**
* Width
* @description The width parameter
*/
width: number;
/**
* Height
* @description The height parameter
*/
height: number;
/**
* Seed
* @description The seed used for noise generation
*/
seed: number;
/**
* Rand Device
* @description The device used for random number generation
*/
rand_device: string;
/**
* Cfg Scale
* @description The classifier-free guidance scale parameter
*/
cfg_scale: number;
/**
* Steps
* @description The number of steps used for inference
*/
steps: number;
/**
* Scheduler
* @description The scheduler used for inference
*/
scheduler: string;
/**
* Clip Skip
* @description The number of skipped CLIP layers
*/
clip_skip: number;
/**
* Model
* @description The main model used for inference
*/
model: components["schemas"]["MainModelField"];
/**
* Controlnets
* @description The ControlNets used for inference
*/
controlnets: (components["schemas"]["ControlField"])[];
/**
* Loras
* @description The LoRAs used for inference
*/
loras: (components["schemas"]["LoRAMetadataField"])[];
/**
* Strength
* @description The strength used for latents-to-latents
*/
strength?: number;
/**
* Init Image
* @description The name of the initial image
*/
init_image?: string;
/**
* Vae
* @description The VAE used for decoding, if the main model's default was not used
*/
vae?: components["schemas"]["VAEModelField"];
};
/**
* MetadataAccumulatorOutput
* @description The output of the MetadataAccumulator node
*/
MetadataAccumulatorOutput: {
/**
* Type
* @default metadata_accumulator_output
* @enum {string}
*/
type?: "metadata_accumulator_output";
/**
* Metadata
* @description The core metadata for the image
*/
metadata: components["schemas"]["CoreMetadata"];
};
/** /**
* MidasDepthImageProcessorInvocation * MidasDepthImageProcessorInvocation
* @description Applies Midas depth processing to image * @description Applies Midas depth processing to image
@ -3110,7 +3272,7 @@ export type components = {
/** ModelsList */ /** ModelsList */
ModelsList: { ModelsList: {
/** Models */ /** Models */
models: (components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"])[]; models: (components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"])[];
}; };
/** /**
* MultiplyInvocation * MultiplyInvocation
@ -3900,14 +4062,14 @@ export type components = {
}; };
/** StableDiffusion1ModelCheckpointConfig */ /** StableDiffusion1ModelCheckpointConfig */
StableDiffusion1ModelCheckpointConfig: { StableDiffusion1ModelCheckpointConfig: {
/** Name */ /** Model Name */
name: string; model_name: string;
base_model: components["schemas"]["BaseModelType"]; base_model: components["schemas"]["BaseModelType"];
/** /**
* Type * Model Type
* @enum {string} * @enum {string}
*/ */
type: "main"; model_type: "main";
/** Path */ /** Path */
path: string; path: string;
/** Description */ /** Description */
@ -3926,14 +4088,14 @@ export type components = {
}; };
/** StableDiffusion1ModelDiffusersConfig */ /** StableDiffusion1ModelDiffusersConfig */
StableDiffusion1ModelDiffusersConfig: { StableDiffusion1ModelDiffusersConfig: {
/** Name */ /** Model Name */
name: string; model_name: string;
base_model: components["schemas"]["BaseModelType"]; base_model: components["schemas"]["BaseModelType"];
/** /**
* Type * Model Type
* @enum {string} * @enum {string}
*/ */
type: "main"; model_type: "main";
/** Path */ /** Path */
path: string; path: string;
/** Description */ /** Description */
@ -3950,14 +4112,14 @@ export type components = {
}; };
/** StableDiffusion2ModelCheckpointConfig */ /** StableDiffusion2ModelCheckpointConfig */
StableDiffusion2ModelCheckpointConfig: { StableDiffusion2ModelCheckpointConfig: {
/** Name */ /** Model Name */
name: string; model_name: string;
base_model: components["schemas"]["BaseModelType"]; base_model: components["schemas"]["BaseModelType"];
/** /**
* Type * Model Type
* @enum {string} * @enum {string}
*/ */
type: "main"; model_type: "main";
/** Path */ /** Path */
path: string; path: string;
/** Description */ /** Description */
@ -3976,14 +4138,14 @@ export type components = {
}; };
/** StableDiffusion2ModelDiffusersConfig */ /** StableDiffusion2ModelDiffusersConfig */
StableDiffusion2ModelDiffusersConfig: { StableDiffusion2ModelDiffusersConfig: {
/** Name */ /** Model Name */
name: string; model_name: string;
base_model: components["schemas"]["BaseModelType"]; base_model: components["schemas"]["BaseModelType"];
/** /**
* Type * Model Type
* @enum {string} * @enum {string}
*/ */
type: "main"; model_type: "main";
/** Path */ /** Path */
path: string; path: string;
/** Description */ /** Description */
@ -4190,14 +4352,14 @@ export type components = {
}; };
/** TextualInversionModelConfig */ /** TextualInversionModelConfig */
TextualInversionModelConfig: { TextualInversionModelConfig: {
/** Name */ /** Model Name */
name: string; model_name: string;
base_model: components["schemas"]["BaseModelType"]; base_model: components["schemas"]["BaseModelType"];
/** /**
* Type * Model Type
* @enum {string} * @enum {string}
*/ */
type: "embedding"; model_type: "embedding";
/** Path */ /** Path */
path: string; path: string;
/** Description */ /** Description */
@ -4367,14 +4529,14 @@ export type components = {
}; };
/** VaeModelConfig */ /** VaeModelConfig */
VaeModelConfig: { VaeModelConfig: {
/** Name */ /** Model Name */
name: string; model_name: string;
base_model: components["schemas"]["BaseModelType"]; base_model: components["schemas"]["BaseModelType"];
/** /**
* Type * Model Type
* @enum {string} * @enum {string}
*/ */
type: "vae"; model_type: "vae";
/** Path */ /** Path */
path: string; path: string;
/** Description */ /** Description */
@ -4425,18 +4587,18 @@ export type components = {
*/ */
image?: components["schemas"]["ImageField"]; image?: components["schemas"]["ImageField"];
}; };
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
/** /**
* StableDiffusion1ModelFormat * StableDiffusion1ModelFormat
* @description An enumeration. * @description An enumeration.
* @enum {string} * @enum {string}
*/ */
StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
}; };
responses: never; responses: never;
parameters: never; parameters: never;
@ -4547,7 +4709,7 @@ export type operations = {
}; };
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]; "application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
}; };
}; };
responses: { responses: {
@ -4584,7 +4746,7 @@ export type operations = {
}; };
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]; "application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
}; };
}; };
responses: { responses: {
@ -4817,7 +4979,7 @@ export type operations = {
/** @description The model imported successfully */ /** @description The model imported successfully */
201: { 201: {
content: { content: {
"application/json": components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"]; "application/json": components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"];
}; };
}; };
/** @description The model could not be found */ /** @description The model could not be found */
@ -4885,14 +5047,14 @@ export type operations = {
}; };
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"]; "application/json": components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"];
}; };
}; };
responses: { responses: {
/** @description The model was updated successfully */ /** @description The model was updated successfully */
200: { 200: {
content: { content: {
"application/json": components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"]; "application/json": components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"];
}; };
}; };
/** @description Bad request */ /** @description Bad request */
@ -4926,7 +5088,7 @@ export type operations = {
/** @description Model converted successfully */ /** @description Model converted successfully */
200: { 200: {
content: { content: {
"application/json": components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"]; "application/json": components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"];
}; };
}; };
/** @description Bad request */ /** @description Bad request */
@ -4961,7 +5123,7 @@ export type operations = {
/** @description Model converted successfully */ /** @description Model converted successfully */
200: { 200: {
content: { content: {
"application/json": components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"]; "application/json": components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"];
}; };
}; };
/** @description Incompatible models */ /** @description Incompatible models */
@ -4977,10 +5139,10 @@ export type operations = {
}; };
}; };
/** /**
* List Images With Metadata * List Image Dtos
* @description Gets a list of images * @description Gets a list of image DTOs
*/ */
list_images_with_metadata: { list_image_dtos: {
parameters: { parameters: {
query?: { query?: {
/** @description The origin of images to list */ /** @description The origin of images to list */
@ -5050,25 +5212,23 @@ export type operations = {
}; };
}; };
/** /**
* Get Image Full * Get Image Dto
* @description Gets a full-resolution image file * @description Gets an image's DTO
*/ */
get_image_full: { get_image_dto: {
parameters: { parameters: {
path: { path: {
/** @description The name of full-resolution image file to get */ /** @description The name of image to get */
image_name: string; image_name: string;
}; };
}; };
responses: { responses: {
/** @description Return the full-resolution image */ /** @description Successful Response */
200: { 200: {
content: { content: {
"image/png": unknown; "application/json": components["schemas"]["ImageDTO"];
}; };
}; };
/** @description Image not found */
404: never;
/** @description Validation Error */ /** @description Validation Error */
422: { 422: {
content: { content: {
@ -5149,7 +5309,7 @@ export type operations = {
/** @description Successful Response */ /** @description Successful Response */
200: { 200: {
content: { content: {
"application/json": components["schemas"]["ImageDTO"]; "application/json": components["schemas"]["ImageMetadata"];
}; };
}; };
/** @description Validation Error */ /** @description Validation Error */
@ -5160,6 +5320,34 @@ export type operations = {
}; };
}; };
}; };
/**
* Get Image Full
* @description Gets a full-resolution image file
*/
get_image_full: {
parameters: {
path: {
/** @description The name of full-resolution image file to get */
image_name: string;
};
};
responses: {
/** @description Return the full-resolution image */
200: {
content: {
"image/png": unknown;
};
};
/** @description Image not found */
404: never;
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
/** /**
* Get Image Thumbnail * Get Image Thumbnail
* @description Gets a thumbnail image file * @description Gets a thumbnail image file

View File

@ -1,9 +1,9 @@
import queryString from 'query-string';
import { createAppAsyncThunk } from 'app/store/storeUtils'; import { createAppAsyncThunk } from 'app/store/storeUtils';
import { selectImagesAll } from 'features/gallery/store/gallerySlice'; import { selectImagesAll } from 'features/gallery/store/gallerySlice';
import { size } from 'lodash-es'; import { size } from 'lodash-es';
import { paths } from 'services/api/schema'; import queryString from 'query-string';
import { $client } from 'services/api/client'; import { $client } from 'services/api/client';
import { paths } from 'services/api/schema';
type GetImageUrlsArg = type GetImageUrlsArg =
paths['/api/v1/images/{image_name}/urls']['get']['parameters']['path']; paths['/api/v1/images/{image_name}/urls']['get']['parameters']['path'];
@ -24,7 +24,7 @@ export const imageUrlsReceived = createAppAsyncThunk<
GetImageUrlsResponse, GetImageUrlsResponse,
GetImageUrlsArg, GetImageUrlsArg,
GetImageUrlsThunkConfig GetImageUrlsThunkConfig
>('api/imageUrlsReceived', async (arg, { rejectWithValue }) => { >('thunkApi/imageUrlsReceived', async (arg, { rejectWithValue }) => {
const { image_name } = arg; const { image_name } = arg;
const { get } = $client.get(); const { get } = $client.get();
const { data, error, response } = await get( const { data, error, response } = await get(
@ -46,10 +46,10 @@ export const imageUrlsReceived = createAppAsyncThunk<
}); });
type GetImageMetadataArg = type GetImageMetadataArg =
paths['/api/v1/images/{image_name}/metadata']['get']['parameters']['path']; paths['/api/v1/images/{image_name}']['get']['parameters']['path'];
type GetImageMetadataResponse = type GetImageMetadataResponse =
paths['/api/v1/images/{image_name}/metadata']['get']['responses']['200']['content']['application/json']; paths['/api/v1/images/{image_name}']['get']['responses']['200']['content']['application/json'];
type GetImageMetadataThunkConfig = { type GetImageMetadataThunkConfig = {
rejectValue: { rejectValue: {
@ -58,21 +58,18 @@ type GetImageMetadataThunkConfig = {
}; };
}; };
export const imageMetadataReceived = createAppAsyncThunk< export const imageDTOReceived = createAppAsyncThunk<
GetImageMetadataResponse, GetImageMetadataResponse,
GetImageMetadataArg, GetImageMetadataArg,
GetImageMetadataThunkConfig GetImageMetadataThunkConfig
>('api/imageMetadataReceived', async (arg, { rejectWithValue }) => { >('thunkApi/imageMetadataReceived', async (arg, { rejectWithValue }) => {
const { image_name } = arg; const { image_name } = arg;
const { get } = $client.get(); const { get } = $client.get();
const { data, error, response } = await get( const { data, error, response } = await get('/api/v1/images/{image_name}', {
'/api/v1/images/{image_name}/metadata', params: {
{ path: { image_name },
params: { },
path: { image_name }, });
},
}
);
if (error) { if (error) {
return rejectWithValue({ arg, error }); return rejectWithValue({ arg, error });
@ -148,7 +145,7 @@ export const imageUploaded = createAppAsyncThunk<
UploadImageResponse, UploadImageResponse,
UploadImageArg, UploadImageArg,
UploadImageThunkConfig UploadImageThunkConfig
>('api/imageUploaded', async (arg, { rejectWithValue }) => { >('thunkApi/imageUploaded', async (arg, { rejectWithValue }) => {
const { const {
postUploadAction, postUploadAction,
file, file,
@ -199,7 +196,7 @@ export const imageDeleted = createAppAsyncThunk<
DeleteImageResponse, DeleteImageResponse,
DeleteImageArg, DeleteImageArg,
DeleteImageThunkConfig DeleteImageThunkConfig
>('api/imageDeleted', async (arg, { rejectWithValue }) => { >('thunkApi/imageDeleted', async (arg, { rejectWithValue }) => {
const { image_name } = arg; const { image_name } = arg;
const { del } = $client.get(); const { del } = $client.get();
const { data, error, response } = await del('/api/v1/images/{image_name}', { const { data, error, response } = await del('/api/v1/images/{image_name}', {
@ -235,7 +232,7 @@ export const imageUpdated = createAppAsyncThunk<
UpdateImageResponse, UpdateImageResponse,
UpdateImageArg, UpdateImageArg,
UpdateImageThunkConfig UpdateImageThunkConfig
>('api/imageUpdated', async (arg, { rejectWithValue }) => { >('thunkApi/imageUpdated', async (arg, { rejectWithValue }) => {
const { image_name, image_category, is_intermediate, session_id } = arg; const { image_name, image_category, is_intermediate, session_id } = arg;
const { patch } = $client.get(); const { patch } = $client.get();
const { data, error, response } = await patch('/api/v1/images/{image_name}', { const { data, error, response } = await patch('/api/v1/images/{image_name}', {
@ -284,46 +281,49 @@ export const receivedPageOfImages = createAppAsyncThunk<
ListImagesResponse, ListImagesResponse,
ListImagesArg, ListImagesArg,
ListImagesThunkConfig ListImagesThunkConfig
>('api/receivedPageOfImages', async (arg, { getState, rejectWithValue }) => { >(
const { get } = $client.get(); 'thunkApi/receivedPageOfImages',
async (arg, { getState, rejectWithValue }) => {
const { get } = $client.get();
const state = getState(); const state = getState();
const { categories, selectedBoardId } = state.gallery; const { categories, selectedBoardId } = state.gallery;
const images = selectImagesAll(state).filter((i) => { const images = selectImagesAll(state).filter((i) => {
const isInCategory = categories.includes(i.image_category); const isInCategory = categories.includes(i.image_category);
const isInSelectedBoard = selectedBoardId const isInSelectedBoard = selectedBoardId
? i.board_id === selectedBoardId ? i.board_id === selectedBoardId
: true; : true;
return isInCategory && isInSelectedBoard; return isInCategory && isInSelectedBoard;
}); });
let query: ListImagesArg = {}; let query: ListImagesArg = {};
if (size(arg)) { if (size(arg)) {
query = { query = {
...DEFAULT_IMAGES_LISTED_ARG, ...DEFAULT_IMAGES_LISTED_ARG,
offset: images.length, offset: images.length,
...arg, ...arg,
}; };
} else { } else {
query = { query = {
...DEFAULT_IMAGES_LISTED_ARG, ...DEFAULT_IMAGES_LISTED_ARG,
categories, categories,
offset: images.length, offset: images.length,
}; };
}
const { data, error, response } = await get('/api/v1/images/', {
params: {
query,
},
querySerializer: (q) => queryString.stringify(q, { arrayFormat: 'none' }),
});
if (error) {
return rejectWithValue({ arg, error });
}
return data;
} }
);
const { data, error, response } = await get('/api/v1/images/', {
params: {
query,
},
querySerializer: (q) => queryString.stringify(q, { arrayFormat: 'none' }),
});
if (error) {
return rejectWithValue({ arg, error });
}
return data;
});

View File

@ -19,6 +19,7 @@ export type ImageChanges = components['schemas']['ImageRecordChanges'];
export type ImageCategory = components['schemas']['ImageCategory']; export type ImageCategory = components['schemas']['ImageCategory'];
export type ResourceOrigin = components['schemas']['ResourceOrigin']; export type ResourceOrigin = components['schemas']['ResourceOrigin'];
export type ImageField = components['schemas']['ImageField']; export type ImageField = components['schemas']['ImageField'];
export type ImageMetadata = components['schemas']['ImageMetadata'];
export type OffsetPaginatedResults_BoardDTO_ = export type OffsetPaginatedResults_BoardDTO_ =
components['schemas']['OffsetPaginatedResults_BoardDTO_']; components['schemas']['OffsetPaginatedResults_BoardDTO_'];
export type OffsetPaginatedResults_ImageDTO_ = export type OffsetPaginatedResults_ImageDTO_ =
@ -31,6 +32,7 @@ export type MainModelField = components['schemas']['MainModelField'];
export type VAEModelField = components['schemas']['VAEModelField']; export type VAEModelField = components['schemas']['VAEModelField'];
export type LoRAModelField = components['schemas']['LoRAModelField']; export type LoRAModelField = components['schemas']['LoRAModelField'];
export type ModelsList = components['schemas']['ModelsList']; export type ModelsList = components['schemas']['ModelsList'];
export type ControlField = components['schemas']['ControlField'];
// Model Configs // Model Configs
export type LoRAModelConfig = components['schemas']['LoRAModelConfig']; export type LoRAModelConfig = components['schemas']['LoRAModelConfig'];
@ -107,6 +109,9 @@ export type MainModelLoaderInvocation = TypeReq<
export type LoraLoaderInvocation = TypeReq< export type LoraLoaderInvocation = TypeReq<
components['schemas']['LoraLoaderInvocation'] components['schemas']['LoraLoaderInvocation']
>; >;
export type MetadataAccumulatorInvocation = TypeReq<
components['schemas']['MetadataAccumulatorInvocation']
>;
// ControlNet Nodes // ControlNet Nodes
export type ControlNetInvocation = TypeReq< export type ControlNetInvocation = TypeReq<

View File

@ -6409,6 +6409,11 @@ use-composed-ref@^1.3.0:
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
use-debounce@^9.0.4:
version "9.0.4"
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.4.tgz#51d25d856fbdfeb537553972ce3943b897f1ac85"
integrity sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==
use-image@^1.1.1: use-image@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/use-image/-/use-image-1.1.1.tgz#bdd3f2e1718393ffc0e56136f993467103d9d2df" resolved "https://registry.yarnpkg.com/use-image/-/use-image-1.1.1.tgz#bdd3f2e1718393ffc0e56136f993467103d9d2df"