mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat: workflow saving and loading
This commit is contained in:
parent
7f6fdf5d39
commit
7d1942e9f0
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
|
import json
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
AbstractSet,
|
AbstractSet,
|
||||||
@ -20,7 +21,7 @@ from typing import (
|
|||||||
get_type_hints,
|
get_type_hints,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, validator
|
||||||
from pydantic.fields import Undefined
|
from pydantic.fields import Undefined
|
||||||
from pydantic.typing import NoArgAnyCallable
|
from pydantic.typing import NoArgAnyCallable
|
||||||
|
|
||||||
@ -141,9 +142,11 @@ class UIType(str, Enum):
|
|||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Misc
|
# region Misc
|
||||||
FilePath = "FilePath"
|
|
||||||
Enum = "enum"
|
Enum = "enum"
|
||||||
Scheduler = "Scheduler"
|
Scheduler = "Scheduler"
|
||||||
|
WorkflowField = "WorkflowField"
|
||||||
|
IsIntermediate = "IsIntermediate"
|
||||||
|
MetadataField = "MetadataField"
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
@ -507,8 +510,24 @@ class BaseInvocation(ABC, BaseModel):
|
|||||||
|
|
||||||
id: str = Field(description="The id of this node. Must be unique among all nodes.")
|
id: str = Field(description="The id of this node. Must be unique among all nodes.")
|
||||||
is_intermediate: bool = InputField(
|
is_intermediate: bool = InputField(
|
||||||
default=False, description="Whether or not this node is an intermediate node.", input=Input.Direct
|
default=False, description="Whether or not this node is an intermediate node.", ui_type=UIType.IsIntermediate
|
||||||
)
|
)
|
||||||
|
workflow: Optional[str] = InputField(
|
||||||
|
default=None,
|
||||||
|
description="The workflow to save with the image",
|
||||||
|
ui_type=UIType.WorkflowField,
|
||||||
|
)
|
||||||
|
|
||||||
|
@validator("workflow", pre=True)
|
||||||
|
def validate_workflow_is_json(cls, v):
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
json.loads(v)
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
raise ValueError("Workflow must be valid JSON")
|
||||||
|
return v
|
||||||
|
|
||||||
UIConfig: ClassVar[Type[UIConfigBase]]
|
UIConfig: ClassVar[Type[UIConfigBase]]
|
||||||
|
|
||||||
|
|
||||||
|
@ -151,11 +151,6 @@ class ImageProcessorInvocation(BaseInvocation):
|
|||||||
# image type should be PIL.PngImagePlugin.PngImageFile ?
|
# image type should be PIL.PngImagePlugin.PngImageFile ?
|
||||||
processed_image = self.run_processor(raw_image)
|
processed_image = self.run_processor(raw_image)
|
||||||
|
|
||||||
# FIXME: what happened to image metadata?
|
|
||||||
# metadata = context.services.metadata.build_metadata(
|
|
||||||
# session_id=context.graph_execution_state_id, node=self
|
|
||||||
# )
|
|
||||||
|
|
||||||
# currently can't see processed image in node UI without a showImage node,
|
# currently can't see processed image in node UI without a showImage node,
|
||||||
# so for now setting image_type to RESULT instead of INTERMEDIATE so will get saved in gallery
|
# so for now setting image_type to RESULT instead of INTERMEDIATE so will get saved in gallery
|
||||||
image_dto = context.services.images.create(
|
image_dto = context.services.images.create(
|
||||||
@ -165,6 +160,7 @@ class ImageProcessorInvocation(BaseInvocation):
|
|||||||
session_id=context.graph_execution_state_id,
|
session_id=context.graph_execution_state_id,
|
||||||
node_id=self.id,
|
node_id=self.id,
|
||||||
is_intermediate=self.is_intermediate,
|
is_intermediate=self.is_intermediate,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
"""Builds an ImageOutput and its ImageField"""
|
"""Builds an ImageOutput and its ImageField"""
|
||||||
|
@ -45,6 +45,7 @@ class CvInpaintInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
|
@ -65,6 +65,7 @@ class BlankImageInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -102,6 +103,7 @@ class ImageCropInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -154,6 +156,7 @@ class ImagePasteInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -189,6 +192,7 @@ class MaskFromAlphaInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -223,6 +227,7 @@ class ImageMultiplyInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -259,6 +264,7 @@ class ImageChannelInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -295,6 +301,7 @@ class ImageConvertInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -333,6 +340,7 @@ class ImageBlurInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -393,6 +401,7 @@ class ImageResizeInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -438,6 +447,7 @@ class ImageScaleInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -475,6 +485,7 @@ class ImageLerpInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -512,6 +523,7 @@ class ImageInverseLerpInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -555,6 +567,7 @@ class ImageNSFWBlurInvocation(BaseInvocation):
|
|||||||
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,
|
metadata=self.metadata.dict() if self.metadata else None,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -596,6 +609,7 @@ class ImageWatermarkInvocation(BaseInvocation):
|
|||||||
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,
|
metadata=self.metadata.dict() if self.metadata else None,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -644,6 +658,7 @@ class MaskEdgeInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -677,6 +692,7 @@ class MaskCombineInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -785,6 +801,7 @@ class ColorCorrectInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -827,6 +844,7 @@ class ImageHueAdjustmentInvocation(BaseInvocation):
|
|||||||
node_id=self.id,
|
node_id=self.id,
|
||||||
is_intermediate=self.is_intermediate,
|
is_intermediate=self.is_intermediate,
|
||||||
session_id=context.graph_execution_state_id,
|
session_id=context.graph_execution_state_id,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -877,6 +895,7 @@ class ImageLuminosityAdjustmentInvocation(BaseInvocation):
|
|||||||
node_id=self.id,
|
node_id=self.id,
|
||||||
is_intermediate=self.is_intermediate,
|
is_intermediate=self.is_intermediate,
|
||||||
session_id=context.graph_execution_state_id,
|
session_id=context.graph_execution_state_id,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -925,6 +944,7 @@ class ImageSaturationAdjustmentInvocation(BaseInvocation):
|
|||||||
node_id=self.id,
|
node_id=self.id,
|
||||||
is_intermediate=self.is_intermediate,
|
is_intermediate=self.is_intermediate,
|
||||||
session_id=context.graph_execution_state_id,
|
session_id=context.graph_execution_state_id,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
|
@ -145,6 +145,7 @@ class InfillColorInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -184,6 +185,7 @@ class InfillTileInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
@ -218,6 +220,7 @@ class InfillPatchMatchInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
|
@ -545,6 +545,7 @@ class LatentsToImageInvocation(BaseInvocation):
|
|||||||
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,
|
metadata=self.metadata.dict() if self.metadata else None,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
|
@ -376,6 +376,7 @@ class ONNXLatentsToImageInvocation(BaseInvocation):
|
|||||||
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,
|
metadata=self.metadata.dict() if self.metadata else None,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
|
@ -7,7 +7,7 @@ from pydantic import validator
|
|||||||
|
|
||||||
from invokeai.app.invocations.primitives import StringCollectionOutput
|
from invokeai.app.invocations.primitives import StringCollectionOutput
|
||||||
|
|
||||||
from .baseinvocation import BaseInvocation, InputField, InvocationContext, UIComponent, UIType, tags, title
|
from .baseinvocation import BaseInvocation, InputField, InvocationContext, UIComponent, tags, title
|
||||||
|
|
||||||
|
|
||||||
@title("Dynamic Prompt")
|
@title("Dynamic Prompt")
|
||||||
@ -41,7 +41,7 @@ class PromptsFromFileInvocation(BaseInvocation):
|
|||||||
type: Literal["prompt_from_file"] = "prompt_from_file"
|
type: Literal["prompt_from_file"] = "prompt_from_file"
|
||||||
|
|
||||||
# Inputs
|
# Inputs
|
||||||
file_path: str = InputField(description="Path to prompt text file", ui_type=UIType.FilePath)
|
file_path: str = InputField(description="Path to prompt text file")
|
||||||
pre_prompt: Optional[str] = InputField(
|
pre_prompt: Optional[str] = InputField(
|
||||||
default=None, description="String to prepend to each prompt", ui_component=UIComponent.Textarea
|
default=None, description="String to prepend to each prompt", ui_component=UIComponent.Textarea
|
||||||
)
|
)
|
||||||
|
@ -110,6 +110,7 @@ class ESRGANInvocation(BaseInvocation):
|
|||||||
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,
|
||||||
|
workflow=self.workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageOutput(
|
return ImageOutput(
|
||||||
|
@ -60,7 +60,7 @@ class ImageFileStorageBase(ABC):
|
|||||||
image: PILImageType,
|
image: PILImageType,
|
||||||
image_name: str,
|
image_name: str,
|
||||||
metadata: Optional[dict] = None,
|
metadata: Optional[dict] = None,
|
||||||
graph: Optional[dict] = None,
|
workflow: Optional[str] = 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,7 +110,7 @@ class DiskImageFileStorage(ImageFileStorageBase):
|
|||||||
image: PILImageType,
|
image: PILImageType,
|
||||||
image_name: str,
|
image_name: str,
|
||||||
metadata: Optional[dict] = None,
|
metadata: Optional[dict] = None,
|
||||||
graph: Optional[dict] = None,
|
workflow: Optional[str] = None,
|
||||||
thumbnail_size: int = 256,
|
thumbnail_size: int = 256,
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
@ -121,8 +121,8 @@ class DiskImageFileStorage(ImageFileStorageBase):
|
|||||||
|
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
pnginfo.add_text("invokeai_metadata", json.dumps(metadata))
|
pnginfo.add_text("invokeai_metadata", json.dumps(metadata))
|
||||||
if graph is not None:
|
if workflow is not None:
|
||||||
pnginfo.add_text("invokeai_graph", json.dumps(graph))
|
pnginfo.add_text("invokeai_workflow", workflow)
|
||||||
|
|
||||||
image.save(image_path, "PNG", pnginfo=pnginfo)
|
image.save(image_path, "PNG", pnginfo=pnginfo)
|
||||||
thumbnail_name = get_thumbnail_name(image_name)
|
thumbnail_name = get_thumbnail_name(image_name)
|
||||||
|
@ -54,6 +54,7 @@ class ImageServiceABC(ABC):
|
|||||||
board_id: Optional[str] = None,
|
board_id: Optional[str] = None,
|
||||||
is_intermediate: bool = False,
|
is_intermediate: bool = False,
|
||||||
metadata: Optional[dict] = None,
|
metadata: Optional[dict] = None,
|
||||||
|
workflow: Optional[str] = None,
|
||||||
) -> ImageDTO:
|
) -> ImageDTO:
|
||||||
"""Creates an image, storing the file and its metadata."""
|
"""Creates an image, storing the file and its metadata."""
|
||||||
pass
|
pass
|
||||||
@ -177,6 +178,7 @@ class ImageService(ImageServiceABC):
|
|||||||
board_id: Optional[str] = None,
|
board_id: Optional[str] = None,
|
||||||
is_intermediate: bool = False,
|
is_intermediate: bool = False,
|
||||||
metadata: Optional[dict] = None,
|
metadata: Optional[dict] = None,
|
||||||
|
workflow: Optional[str] = None,
|
||||||
) -> ImageDTO:
|
) -> ImageDTO:
|
||||||
if image_origin not in ResourceOrigin:
|
if image_origin not in ResourceOrigin:
|
||||||
raise InvalidOriginException
|
raise InvalidOriginException
|
||||||
@ -186,16 +188,16 @@ class ImageService(ImageServiceABC):
|
|||||||
|
|
||||||
image_name = self._services.names.create_image_name()
|
image_name = self._services.names.create_image_name()
|
||||||
|
|
||||||
graph = None
|
# TODO: Do we want to store the graph in the image at all? I don't think so...
|
||||||
|
# graph = None
|
||||||
if session_id is not None:
|
# if session_id is not None:
|
||||||
session_raw = self._services.graph_execution_manager.get_raw(session_id)
|
# session_raw = self._services.graph_execution_manager.get_raw(session_id)
|
||||||
if session_raw is not None:
|
# if session_raw is not None:
|
||||||
try:
|
# try:
|
||||||
graph = get_metadata_graph_from_raw_session(session_raw)
|
# graph = get_metadata_graph_from_raw_session(session_raw)
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
self._services.logger.warn(f"Failed to parse session graph: {e}")
|
# self._services.logger.warn(f"Failed to parse session graph: {e}")
|
||||||
graph = None
|
# graph = None
|
||||||
|
|
||||||
(width, height) = image.size
|
(width, height) = image.size
|
||||||
|
|
||||||
@ -217,7 +219,7 @@ class ImageService(ImageServiceABC):
|
|||||||
)
|
)
|
||||||
if board_id is not None:
|
if board_id is not None:
|
||||||
self._services.board_image_records.add_image_to_board(board_id=board_id, image_name=image_name)
|
self._services.board_image_records.add_image_to_board(board_id=board_id, image_name=image_name)
|
||||||
self._services.image_files.save(image_name=image_name, image=image, metadata=metadata, graph=graph)
|
self._services.image_files.save(image_name=image_name, image=image, metadata=metadata, workflow=workflow)
|
||||||
image_dto = self.get_dto(image_name)
|
image_dto = self.get_dto(image_name)
|
||||||
|
|
||||||
return image_dto
|
return image_dto
|
||||||
|
@ -7,5 +7,4 @@ stats.html
|
|||||||
index.html
|
index.html
|
||||||
.yarn/
|
.yarn/
|
||||||
*.scss
|
*.scss
|
||||||
src/services/api/
|
src/services/api/schema.d.ts
|
||||||
src/services/fixtures/*
|
|
||||||
|
@ -7,8 +7,7 @@ index.html
|
|||||||
.yarn/
|
.yarn/
|
||||||
.yalc/
|
.yalc/
|
||||||
*.scss
|
*.scss
|
||||||
src/services/api/
|
src/services/api/schema.d.ts
|
||||||
src/services/fixtures/*
|
|
||||||
docs/
|
docs/
|
||||||
static/
|
static/
|
||||||
src/theme/css/overlayscrollbars.css
|
src/theme/css/overlayscrollbars.css
|
||||||
|
@ -74,6 +74,7 @@
|
|||||||
"@nanostores/react": "^0.7.1",
|
"@nanostores/react": "^0.7.1",
|
||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"@roarr/browser-log-writer": "^1.1.5",
|
"@roarr/browser-log-writer": "^1.1.5",
|
||||||
|
"@stevebel/png": "^1.5.1",
|
||||||
"dateformat": "^5.0.3",
|
"dateformat": "^5.0.3",
|
||||||
"formik": "^2.4.3",
|
"formik": "^2.4.3",
|
||||||
"framer-motion": "^10.16.1",
|
"framer-motion": "^10.16.1",
|
||||||
|
@ -716,7 +716,7 @@
|
|||||||
},
|
},
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"reloadNodeTemplates": "Reload Node Templates",
|
"reloadNodeTemplates": "Reload Node Templates",
|
||||||
"saveWorkflow": "Save Workflow",
|
"downloadWorkflow": "Download Workflow JSON",
|
||||||
"loadWorkflow": "Load Workflow",
|
"loadWorkflow": "Load Workflow",
|
||||||
"resetWorkflow": "Reset Workflow",
|
"resetWorkflow": "Reset Workflow",
|
||||||
"resetWorkflowDesc": "Are you sure you want to reset this workflow?",
|
"resetWorkflowDesc": "Are you sure you want to reset this workflow?",
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { selectIsBusy } from 'features/system/store/systemSelectors';
|
import { selectIsBusy } from 'features/system/store/systemSelectors';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
@ -18,8 +20,6 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useUploadImageMutation } from 'services/api/endpoints/images';
|
import { useUploadImageMutation } from 'services/api/endpoints/images';
|
||||||
import { PostUploadAction } from 'services/api/types';
|
import { PostUploadAction } from 'services/api/types';
|
||||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector, activeTabNameSelector],
|
[stateSelector, activeTabNameSelector],
|
||||||
|
@ -9,20 +9,24 @@ import {
|
|||||||
MenuButton,
|
MenuButton,
|
||||||
MenuList,
|
MenuList,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
|
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
|
||||||
import ParamUpscalePopover from 'features/parameters/components/Parameters/Upscale/ParamUpscaleSettings';
|
import ParamUpscalePopover from 'features/parameters/components/Parameters/Upscale/ParamUpscaleSettings';
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
import { makeToast } from 'features/system/util/makeToast';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import {
|
import {
|
||||||
|
setActiveTab,
|
||||||
setShouldShowImageDetails,
|
setShouldShowImageDetails,
|
||||||
setShouldShowProgressInViewer,
|
setShouldShowProgressInViewer,
|
||||||
} from 'features/ui/store/uiSlice';
|
} from 'features/ui/store/uiSlice';
|
||||||
@ -37,12 +41,12 @@ import {
|
|||||||
FaSeedling,
|
FaSeedling,
|
||||||
FaShareAlt,
|
FaShareAlt,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
|
import { MdDeviceHub } from 'react-icons/md';
|
||||||
import {
|
import {
|
||||||
useGetImageDTOQuery,
|
useGetImageDTOQuery,
|
||||||
useGetImageMetadataQuery,
|
useGetImageMetadataFromFileQuery,
|
||||||
} from 'services/api/endpoints/images';
|
} from 'services/api/endpoints/images';
|
||||||
import { menuListMotionProps } from 'theme/components/menu';
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
import { useDebounce } from 'use-debounce';
|
|
||||||
import { sentImageToImg2Img } from '../../store/actions';
|
import { sentImageToImg2Img } from '../../store/actions';
|
||||||
import SingleSelectionMenuItems from '../ImageContextMenu/SingleSelectionMenuItems';
|
import SingleSelectionMenuItems from '../ImageContextMenu/SingleSelectionMenuItems';
|
||||||
|
|
||||||
@ -101,22 +105,36 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
||||||
useRecallParameters();
|
useRecallParameters();
|
||||||
|
|
||||||
const [debouncedMetadataQueryArg, debounceState] = useDebounce(
|
|
||||||
lastSelectedImage,
|
|
||||||
500
|
|
||||||
);
|
|
||||||
|
|
||||||
const { currentData: imageDTO } = useGetImageDTOQuery(
|
const { currentData: imageDTO } = useGetImageDTOQuery(
|
||||||
lastSelectedImage?.image_name ?? skipToken
|
lastSelectedImage?.image_name ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
const { currentData: metadataData } = useGetImageMetadataQuery(
|
const { metadata, workflow, isLoading } = useGetImageMetadataFromFileQuery(
|
||||||
debounceState.isPending()
|
lastSelectedImage?.image_name ?? skipToken,
|
||||||
? skipToken
|
{
|
||||||
: debouncedMetadataQueryArg?.image_name ?? skipToken
|
selectFromResult: (res) => ({
|
||||||
|
isLoading: res.isFetching,
|
||||||
|
metadata: res?.currentData?.metadata,
|
||||||
|
workflow: res?.currentData?.workflow,
|
||||||
|
}),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const metadata = metadataData?.metadata;
|
const handleLoadWorkflow = useCallback(() => {
|
||||||
|
if (!workflow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(workflowLoaded(workflow));
|
||||||
|
dispatch(setActiveTab('nodes'));
|
||||||
|
dispatch(
|
||||||
|
addToast(
|
||||||
|
makeToast({
|
||||||
|
title: 'Workflow Loaded',
|
||||||
|
status: 'success',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [dispatch, workflow]);
|
||||||
|
|
||||||
const handleClickUseAllParameters = useCallback(() => {
|
const handleClickUseAllParameters = useCallback(() => {
|
||||||
recallAllParameters(metadata);
|
recallAllParameters(metadata);
|
||||||
@ -153,6 +171,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
|
|
||||||
useHotkeys('p', handleUsePrompt, [imageDTO]);
|
useHotkeys('p', handleUsePrompt, [imageDTO]);
|
||||||
|
|
||||||
|
useHotkeys('w', handleLoadWorkflow, [workflow]);
|
||||||
|
|
||||||
const handleSendToImageToImage = useCallback(() => {
|
const handleSendToImageToImage = useCallback(() => {
|
||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(initialImageSelected(imageDTO));
|
dispatch(initialImageSelected(imageDTO));
|
||||||
@ -259,22 +279,31 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
|
|
||||||
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
|
isLoading={isLoading}
|
||||||
|
icon={<MdDeviceHub />}
|
||||||
|
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||||
|
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||||
|
isDisabled={!workflow}
|
||||||
|
onClick={handleLoadWorkflow}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
isLoading={isLoading}
|
||||||
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={!metadata?.positive_prompt}
|
isDisabled={!metadata?.positive_prompt}
|
||||||
onClick={handleUsePrompt}
|
onClick={handleUsePrompt}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
|
isLoading={isLoading}
|
||||||
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={!metadata?.seed}
|
isDisabled={!metadata?.seed}
|
||||||
onClick={handleUseSeed}
|
onClick={handleUseSeed}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
|
isLoading={isLoading}
|
||||||
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)`}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { MenuItem } from '@chakra-ui/react';
|
import { Flex, MenuItem, Spinner } from '@chakra-ui/react';
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
@ -8,9 +7,12 @@ import {
|
|||||||
isModalOpenChanged,
|
isModalOpenChanged,
|
||||||
} from 'features/changeBoardModal/store/slice';
|
} from 'features/changeBoardModal/store/slice';
|
||||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
|
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
import { makeToast } from 'features/system/util/makeToast';
|
||||||
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
@ -26,14 +28,13 @@ import {
|
|||||||
FaShare,
|
FaShare,
|
||||||
FaTrash,
|
FaTrash,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
import { MdDeviceHub, MdStar, MdStarBorder } from 'react-icons/md';
|
||||||
import {
|
import {
|
||||||
useGetImageMetadataQuery,
|
useGetImageMetadataFromFileQuery,
|
||||||
useStarImagesMutation,
|
useStarImagesMutation,
|
||||||
useUnstarImagesMutation,
|
useUnstarImagesMutation,
|
||||||
} from 'services/api/endpoints/images';
|
} from 'services/api/endpoints/images';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { useDebounce } from 'use-debounce';
|
|
||||||
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||||
|
|
||||||
type SingleSelectionMenuItemsProps = {
|
type SingleSelectionMenuItemsProps = {
|
||||||
@ -50,15 +51,15 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
|
|
||||||
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
||||||
|
|
||||||
const [debouncedMetadataQueryArg, debounceState] = useDebounce(
|
const { metadata, workflow, isLoading } = useGetImageMetadataFromFileQuery(
|
||||||
imageDTO.image_name,
|
imageDTO.image_name,
|
||||||
500
|
{
|
||||||
);
|
selectFromResult: (res) => ({
|
||||||
|
isLoading: res.isFetching,
|
||||||
const { currentData } = useGetImageMetadataQuery(
|
metadata: res?.currentData?.metadata,
|
||||||
debounceState.isPending()
|
workflow: res?.currentData?.workflow,
|
||||||
? skipToken
|
}),
|
||||||
: debouncedMetadataQueryArg ?? skipToken
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const [starImages] = useStarImagesMutation();
|
const [starImages] = useStarImagesMutation();
|
||||||
@ -67,8 +68,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
const { isClipboardAPIAvailable, copyImageToClipboard } =
|
const { isClipboardAPIAvailable, copyImageToClipboard } =
|
||||||
useCopyImageToClipboard();
|
useCopyImageToClipboard();
|
||||||
|
|
||||||
const metadata = currentData?.metadata;
|
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
if (!imageDTO) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
@ -99,6 +98,22 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
recallSeed(metadata?.seed);
|
recallSeed(metadata?.seed);
|
||||||
}, [metadata?.seed, recallSeed]);
|
}, [metadata?.seed, recallSeed]);
|
||||||
|
|
||||||
|
const handleLoadWorkflow = useCallback(() => {
|
||||||
|
if (!workflow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(workflowLoaded(workflow));
|
||||||
|
dispatch(setActiveTab('nodes'));
|
||||||
|
dispatch(
|
||||||
|
addToast(
|
||||||
|
makeToast({
|
||||||
|
title: 'Workflow Loaded',
|
||||||
|
status: 'success',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [dispatch, workflow]);
|
||||||
|
|
||||||
const handleSendToImageToImage = useCallback(() => {
|
const handleSendToImageToImage = useCallback(() => {
|
||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(initialImageSelected(imageDTO));
|
dispatch(initialImageSelected(imageDTO));
|
||||||
@ -118,7 +133,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
}, [dispatch, imageDTO, t, toaster]);
|
}, [dispatch, imageDTO, t, toaster]);
|
||||||
|
|
||||||
const handleUseAllParameters = useCallback(() => {
|
const handleUseAllParameters = useCallback(() => {
|
||||||
console.log(metadata);
|
|
||||||
recallAllParameters(metadata);
|
recallAllParameters(metadata);
|
||||||
}, [metadata, recallAllParameters]);
|
}, [metadata, recallAllParameters]);
|
||||||
|
|
||||||
@ -169,27 +183,34 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
{t('parameters.downloadImage')}
|
{t('parameters.downloadImage')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<FaQuoteRight />}
|
icon={isLoading ? <SpinnerIcon /> : <MdDeviceHub />}
|
||||||
|
onClickCapture={handleLoadWorkflow}
|
||||||
|
isDisabled={isLoading || !workflow}
|
||||||
|
>
|
||||||
|
{t('nodes.loadWorkflow')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={isLoading ? <SpinnerIcon /> : <FaQuoteRight />}
|
||||||
onClickCapture={handleRecallPrompt}
|
onClickCapture={handleRecallPrompt}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
metadata?.positive_prompt === undefined &&
|
isLoading ||
|
||||||
metadata?.negative_prompt === undefined
|
(metadata?.positive_prompt === undefined &&
|
||||||
|
metadata?.negative_prompt === undefined)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('parameters.usePrompt')}
|
{t('parameters.usePrompt')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<FaSeedling />}
|
icon={isLoading ? <SpinnerIcon /> : <FaSeedling />}
|
||||||
onClickCapture={handleRecallSeed}
|
onClickCapture={handleRecallSeed}
|
||||||
isDisabled={metadata?.seed === undefined}
|
isDisabled={isLoading || metadata?.seed === undefined}
|
||||||
>
|
>
|
||||||
{t('parameters.useSeed')}
|
{t('parameters.useSeed')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<FaAsterisk />}
|
icon={isLoading ? <SpinnerIcon /> : <FaAsterisk />}
|
||||||
onClickCapture={handleUseAllParameters}
|
onClickCapture={handleUseAllParameters}
|
||||||
isDisabled={!metadata}
|
isDisabled={isLoading || !metadata}
|
||||||
>
|
>
|
||||||
{t('parameters.useAll')}
|
{t('parameters.useAll')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -233,3 +254,9 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(SingleSelectionMenuItems);
|
export default memo(SingleSelectionMenuItems);
|
||||||
|
|
||||||
|
const SpinnerIcon = () => (
|
||||||
|
<Flex w="14px" alignItems="center" justifyContent="center">
|
||||||
|
<Spinner size="xs" />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
@ -2,7 +2,7 @@ import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react';
|
|||||||
import { isString } from 'lodash-es';
|
import { isString } from 'lodash-es';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { FaCopy, FaSave } from 'react-icons/fa';
|
import { FaCopy, FaDownload } from 'react-icons/fa';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -23,7 +23,7 @@ const DataViewer = (props: Props) => {
|
|||||||
navigator.clipboard.writeText(dataString);
|
navigator.clipboard.writeText(dataString);
|
||||||
}, [dataString]);
|
}, [dataString]);
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleDownload = useCallback(() => {
|
||||||
const blob = new Blob([dataString]);
|
const blob = new Blob([dataString]);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(blob);
|
||||||
@ -73,13 +73,13 @@ const DataViewer = (props: Props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
|
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
|
||||||
{withDownload && (
|
{withDownload && (
|
||||||
<Tooltip label={`Save ${label} JSON`}>
|
<Tooltip label={`Download ${label} JSON`}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`Save ${label} JSON`}
|
aria-label={`Download ${label} JSON`}
|
||||||
icon={<FaSave />}
|
icon={<FaDownload />}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
opacity={0.7}
|
opacity={0.7}
|
||||||
onClick={handleSave}
|
onClick={handleDownload}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
import { CoreMetadata } from 'features/nodes/types/types';
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { UnsafeImageMetadata } from 'services/api/types';
|
|
||||||
import ImageMetadataItem from './ImageMetadataItem';
|
import ImageMetadataItem from './ImageMetadataItem';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
metadata?: UnsafeImageMetadata['metadata'];
|
metadata?: CoreMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageMetadataActions = (props: Props) => {
|
const ImageMetadataActions = (props: Props) => {
|
||||||
@ -91,14 +91,14 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
onClick={handleRecallNegativePrompt}
|
onClick={handleRecallNegativePrompt}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.seed !== undefined && (
|
{metadata.seed !== undefined && metadata.seed !== null && (
|
||||||
<ImageMetadataItem
|
<ImageMetadataItem
|
||||||
label="Seed"
|
label="Seed"
|
||||||
value={metadata.seed}
|
value={metadata.seed}
|
||||||
onClick={handleRecallSeed}
|
onClick={handleRecallSeed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.model !== undefined && (
|
{metadata.model !== undefined && metadata.model !== null && (
|
||||||
<ImageMetadataItem
|
<ImageMetadataItem
|
||||||
label="Model"
|
label="Model"
|
||||||
value={metadata.model.model_name}
|
value={metadata.model.model_name}
|
||||||
@ -147,7 +147,7 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
onClick={handleRecallSteps}
|
onClick={handleRecallSteps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.cfg_scale !== undefined && (
|
{metadata.cfg_scale !== undefined && metadata.cfg_scale !== null && (
|
||||||
<ImageMetadataItem
|
<ImageMetadataItem
|
||||||
label="CFG scale"
|
label="CFG scale"
|
||||||
value={metadata.cfg_scale}
|
value={metadata.cfg_scale}
|
||||||
|
@ -9,14 +9,12 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
import { useGetImageMetadataFromFileQuery } from 'services/api/endpoints/images';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { useDebounce } from 'use-debounce';
|
|
||||||
import ImageMetadataActions from './ImageMetadataActions';
|
|
||||||
import DataViewer from './DataViewer';
|
import DataViewer from './DataViewer';
|
||||||
|
import ImageMetadataActions from './ImageMetadataActions';
|
||||||
|
|
||||||
type ImageMetadataViewerProps = {
|
type ImageMetadataViewerProps = {
|
||||||
image: ImageDTO;
|
image: ImageDTO;
|
||||||
@ -29,19 +27,16 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
|||||||
// dispatch(setShouldShowImageDetails(false));
|
// dispatch(setShouldShowImageDetails(false));
|
||||||
// });
|
// });
|
||||||
|
|
||||||
const [debouncedMetadataQueryArg, debounceState] = useDebounce(
|
const { metadata, workflow } = useGetImageMetadataFromFileQuery(
|
||||||
image.image_name,
|
image.image_name,
|
||||||
500
|
{
|
||||||
|
selectFromResult: (res) => ({
|
||||||
|
metadata: res?.currentData?.metadata,
|
||||||
|
workflow: res?.currentData?.workflow,
|
||||||
|
}),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { currentData } = useGetImageMetadataQuery(
|
|
||||||
debounceState.isPending()
|
|
||||||
? skipToken
|
|
||||||
: debouncedMetadataQueryArg ?? skipToken
|
|
||||||
);
|
|
||||||
const metadata = currentData?.metadata;
|
|
||||||
const graph = currentData?.graph;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
layerStyle="first"
|
layerStyle="first"
|
||||||
@ -71,17 +66,17 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
|||||||
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}
|
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}
|
||||||
>
|
>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab>Core Metadata</Tab>
|
<Tab>Metadata</Tab>
|
||||||
<Tab>Image Details</Tab>
|
<Tab>Image Details</Tab>
|
||||||
<Tab>Graph</Tab>
|
<Tab>Workflow</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
{metadata ? (
|
{metadata ? (
|
||||||
<DataViewer data={metadata} label="Core Metadata" />
|
<DataViewer data={metadata} label="Metadata" />
|
||||||
) : (
|
) : (
|
||||||
<IAINoContentFallback label="No core metadata found" />
|
<IAINoContentFallback label="No metadata found" />
|
||||||
)}
|
)}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
@ -92,10 +87,10 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
|||||||
)}
|
)}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
{graph ? (
|
{workflow ? (
|
||||||
<DataViewer data={graph} label="Graph" />
|
<DataViewer data={workflow} label="Workflow" />
|
||||||
) : (
|
) : (
|
||||||
<IAINoContentFallback label="No graph found" />
|
<IAINoContentFallback label="No workflow found" />
|
||||||
)}
|
)}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
import { Checkbox, Flex, FormControl, FormLabel } from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { useEmbedWorkflow } from 'features/nodes/hooks/useEmbedWorkflow';
|
||||||
|
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
|
||||||
|
import { nodeEmbedWorkflowChanged } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { ChangeEvent, memo, useCallback } from 'react';
|
||||||
|
|
||||||
|
const EmbedWorkflowCheckbox = ({ nodeId }: { nodeId: string }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const hasImageOutput = useHasImageOutput(nodeId);
|
||||||
|
const embedWorkflow = useEmbedWorkflow(nodeId);
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(
|
||||||
|
nodeEmbedWorkflowChanged({
|
||||||
|
nodeId,
|
||||||
|
embedWorkflow: e.target.checked,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasImageOutput) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
|
||||||
|
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>Embed Workflow</FormLabel>
|
||||||
|
<Checkbox
|
||||||
|
className="nopan"
|
||||||
|
size="sm"
|
||||||
|
onChange={handleChange}
|
||||||
|
isChecked={embedWorkflow}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(EmbedWorkflowCheckbox);
|
@ -1,16 +1,8 @@
|
|||||||
import {
|
import { Flex } from '@chakra-ui/react';
|
||||||
Checkbox,
|
|
||||||
Flex,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Spacer,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
|
|
||||||
import { useIsIntermediate } from 'features/nodes/hooks/useIsIntermediate';
|
|
||||||
import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||||
import { ChangeEvent, memo, useCallback } from 'react';
|
import { memo } from 'react';
|
||||||
|
import EmbedWorkflowCheckbox from './EmbedWorkflowCheckbox';
|
||||||
|
import SaveToGalleryCheckbox from './SaveToGalleryCheckbox';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@ -27,48 +19,13 @@ const InvocationNodeFooter = ({ nodeId }: Props) => {
|
|||||||
px: 2,
|
px: 2,
|
||||||
py: 0,
|
py: 0,
|
||||||
h: 6,
|
h: 6,
|
||||||
|
justifyContent: 'space-between',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Spacer />
|
<EmbedWorkflowCheckbox nodeId={nodeId} />
|
||||||
<SaveImageCheckbox nodeId={nodeId} />
|
<SaveToGalleryCheckbox nodeId={nodeId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(InvocationNodeFooter);
|
export default memo(InvocationNodeFooter);
|
||||||
|
|
||||||
const SaveImageCheckbox = memo(({ nodeId }: { nodeId: string }) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const hasImageOutput = useHasImageOutput(nodeId);
|
|
||||||
const is_intermediate = useIsIntermediate(nodeId);
|
|
||||||
const handleChangeIsIntermediate = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
dispatch(
|
|
||||||
fieldBooleanValueChanged({
|
|
||||||
nodeId,
|
|
||||||
fieldName: 'is_intermediate',
|
|
||||||
value: !e.target.checked,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[dispatch, nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasImageOutput) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
|
|
||||||
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>Save Output</FormLabel>
|
|
||||||
<Checkbox
|
|
||||||
className="nopan"
|
|
||||||
size="sm"
|
|
||||||
onChange={handleChangeIsIntermediate}
|
|
||||||
isChecked={!is_intermediate}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
SaveImageCheckbox.displayName = 'SaveImageCheckbox';
|
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
import { Checkbox, Flex, FormControl, FormLabel } from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
|
||||||
|
import { useIsIntermediate } from 'features/nodes/hooks/useIsIntermediate';
|
||||||
|
import { nodeIsIntermediateChanged } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { ChangeEvent, memo, useCallback } from 'react';
|
||||||
|
|
||||||
|
const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const hasImageOutput = useHasImageOutput(nodeId);
|
||||||
|
const isIntermediate = useIsIntermediate(nodeId);
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(
|
||||||
|
nodeIsIntermediateChanged({
|
||||||
|
nodeId,
|
||||||
|
isIntermediate: !e.target.checked,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasImageOutput) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
|
||||||
|
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>Save to Gallery</FormLabel>
|
||||||
|
<Checkbox
|
||||||
|
className="nopan"
|
||||||
|
size="sm"
|
||||||
|
onChange={handleChange}
|
||||||
|
isChecked={!isIntermediate}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SaveToGalleryCheckbox);
|
@ -2,12 +2,12 @@ import IAIIconButton from 'common/components/IAIIconButton';
|
|||||||
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaSave } from 'react-icons/fa';
|
import { FaDownload } from 'react-icons/fa';
|
||||||
|
|
||||||
const SaveWorkflowButton = () => {
|
const DownloadWorkflowButton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const workflow = useWorkflow();
|
const workflow = useWorkflow();
|
||||||
const handleSave = useCallback(() => {
|
const handleDownload = useCallback(() => {
|
||||||
const blob = new Blob([JSON.stringify(workflow, null, 2)]);
|
const blob = new Blob([JSON.stringify(workflow, null, 2)]);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(blob);
|
||||||
@ -18,12 +18,12 @@ const SaveWorkflowButton = () => {
|
|||||||
}, [workflow]);
|
}, [workflow]);
|
||||||
return (
|
return (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
icon={<FaSave />}
|
icon={<FaDownload />}
|
||||||
tooltip={t('nodes.saveWorkflow')}
|
tooltip={t('nodes.downloadWorkflow')}
|
||||||
aria-label={t('nodes.saveWorkflow')}
|
aria-label={t('nodes.downloadWorkflow')}
|
||||||
onClick={handleSave}
|
onClick={handleDownload}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(SaveWorkflowButton);
|
export default memo(DownloadWorkflowButton);
|
@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/layout';
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import LoadWorkflowButton from './LoadWorkflowButton';
|
import LoadWorkflowButton from './LoadWorkflowButton';
|
||||||
import ResetWorkflowButton from './ResetWorkflowButton';
|
import ResetWorkflowButton from './ResetWorkflowButton';
|
||||||
import SaveWorkflowButton from './SaveWorkflowButton';
|
import DownloadWorkflowButton from './DownloadWorkflowButton';
|
||||||
|
|
||||||
const TopCenterPanel = () => {
|
const TopCenterPanel = () => {
|
||||||
return (
|
return (
|
||||||
@ -15,7 +15,7 @@ const TopCenterPanel = () => {
|
|||||||
transform: 'translate(-50%)',
|
transform: 'translate(-50%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SaveWorkflowButton />
|
<DownloadWorkflowButton />
|
||||||
<LoadWorkflowButton />
|
<LoadWorkflowButton />
|
||||||
<ResetWorkflowButton />
|
<ResetWorkflowButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -22,6 +22,7 @@ export const useAnyOrDirectInputFieldNames = (nodeId: string) => {
|
|||||||
}
|
}
|
||||||
return map(nodeTemplate.inputs)
|
return map(nodeTemplate.inputs)
|
||||||
.filter((field) => ['any', 'direct'].includes(field.input))
|
.filter((field) => ['any', 'direct'].includes(field.input))
|
||||||
|
.filter((field) => !field.ui_hidden)
|
||||||
.sort((a, b) => (a.ui_order ?? 0) - (b.ui_order ?? 0))
|
.sort((a, b) => (a.ui_order ?? 0) - (b.ui_order ?? 0))
|
||||||
.map((field) => field.name)
|
.map((field) => field.name)
|
||||||
.filter((fieldName) => fieldName !== 'is_intermediate');
|
.filter((fieldName) => fieldName !== 'is_intermediate');
|
||||||
|
@ -143,6 +143,8 @@ export const useBuildNodeData = () => {
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
label: '',
|
label: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
|
embedWorkflow: false,
|
||||||
|
isIntermediate: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ export const useConnectionInputFieldNames = (nodeId: string) => {
|
|||||||
}
|
}
|
||||||
return map(nodeTemplate.inputs)
|
return map(nodeTemplate.inputs)
|
||||||
.filter((field) => field.input === 'connection')
|
.filter((field) => field.input === 'connection')
|
||||||
|
.filter((field) => !field.ui_hidden)
|
||||||
.sort((a, b) => (a.ui_order ?? 0) - (b.ui_order ?? 0))
|
.sort((a, b) => (a.ui_order ?? 0) - (b.ui_order ?? 0))
|
||||||
.map((field) => field.name)
|
.map((field) => field.name)
|
||||||
.filter((fieldName) => fieldName !== 'is_intermediate');
|
.filter((fieldName) => fieldName !== 'is_intermediate');
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { isInvocationNode } from '../types/types';
|
||||||
|
|
||||||
|
export const useEmbedWorkflow = (nodeId: string) => {
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ nodes }) => {
|
||||||
|
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||||
|
if (!isInvocationNode(node)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return node.data.embedWorkflow;
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const embedWorkflow = useAppSelector(selector);
|
||||||
|
return embedWorkflow;
|
||||||
|
};
|
@ -15,7 +15,7 @@ export const useIsIntermediate = (nodeId: string) => {
|
|||||||
if (!isInvocationNode(node)) {
|
if (!isInvocationNode(node)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return Boolean(node.data.inputs.is_intermediate?.value);
|
return node.data.isIntermediate;
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
),
|
),
|
||||||
|
@ -21,6 +21,7 @@ export const useOutputFieldNames = (nodeId: string) => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return map(nodeTemplate.outputs)
|
return map(nodeTemplate.outputs)
|
||||||
|
.filter((field) => !field.ui_hidden)
|
||||||
.sort((a, b) => (a.ui_order ?? 0) - (b.ui_order ?? 0))
|
.sort((a, b) => (a.ui_order ?? 0) - (b.ui_order ?? 0))
|
||||||
.map((field) => field.name)
|
.map((field) => field.name)
|
||||||
.filter((fieldName) => fieldName !== 'is_intermediate');
|
.filter((fieldName) => fieldName !== 'is_intermediate');
|
||||||
|
@ -245,6 +245,34 @@ const nodesSlice = createSlice({
|
|||||||
}
|
}
|
||||||
field.label = label;
|
field.label = label;
|
||||||
},
|
},
|
||||||
|
nodeEmbedWorkflowChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ nodeId: string; embedWorkflow: boolean }>
|
||||||
|
) => {
|
||||||
|
const { nodeId, embedWorkflow } = action.payload;
|
||||||
|
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
|
||||||
|
|
||||||
|
const node = state.nodes?.[nodeIndex];
|
||||||
|
|
||||||
|
if (!isInvocationNode(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.data.embedWorkflow = embedWorkflow;
|
||||||
|
},
|
||||||
|
nodeIsIntermediateChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ nodeId: string; isIntermediate: boolean }>
|
||||||
|
) => {
|
||||||
|
const { nodeId, isIntermediate } = action.payload;
|
||||||
|
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
|
||||||
|
|
||||||
|
const node = state.nodes?.[nodeIndex];
|
||||||
|
|
||||||
|
if (!isInvocationNode(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.data.isIntermediate = isIntermediate;
|
||||||
|
},
|
||||||
nodeIsOpenChanged: (
|
nodeIsOpenChanged: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ nodeId: string; isOpen: boolean }>
|
action: PayloadAction<{ nodeId: string; isOpen: boolean }>
|
||||||
@ -850,6 +878,8 @@ export const {
|
|||||||
addNodePopoverClosed,
|
addNodePopoverClosed,
|
||||||
addNodePopoverToggled,
|
addNodePopoverToggled,
|
||||||
selectionModeChanged,
|
selectionModeChanged,
|
||||||
|
nodeEmbedWorkflowChanged,
|
||||||
|
nodeIsIntermediateChanged,
|
||||||
} = nodesSlice.actions;
|
} = nodesSlice.actions;
|
||||||
|
|
||||||
export default nodesSlice.reducer;
|
export default nodesSlice.reducer;
|
||||||
|
@ -169,11 +169,6 @@ export const FIELDS: Record<FieldType, FieldUIConfig> = {
|
|||||||
title: 'Color Collection',
|
title: 'Color Collection',
|
||||||
description: 'A collection of colors.',
|
description: 'A collection of colors.',
|
||||||
},
|
},
|
||||||
FilePath: {
|
|
||||||
color: 'base.500',
|
|
||||||
title: 'File Path',
|
|
||||||
description: 'A path to a file.',
|
|
||||||
},
|
|
||||||
ONNXModelField: {
|
ONNXModelField: {
|
||||||
color: 'base.500',
|
color: 'base.500',
|
||||||
title: 'ONNX Model',
|
title: 'ONNX Model',
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
SchedulerParam,
|
SchedulerParam,
|
||||||
zBaseModel,
|
zBaseModel,
|
||||||
zMainOrOnnxModel,
|
zMainOrOnnxModel,
|
||||||
|
zSDXLRefinerModel,
|
||||||
zScheduler,
|
zScheduler,
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
import { OpenAPIV3 } from 'openapi-types';
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
@ -97,7 +98,6 @@ export const zFieldType = z.enum([
|
|||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Misc
|
// region Misc
|
||||||
'FilePath',
|
|
||||||
'enum',
|
'enum',
|
||||||
'Scheduler',
|
'Scheduler',
|
||||||
// endregion
|
// endregion
|
||||||
@ -105,8 +105,17 @@ export const zFieldType = z.enum([
|
|||||||
|
|
||||||
export type FieldType = z.infer<typeof zFieldType>;
|
export type FieldType = z.infer<typeof zFieldType>;
|
||||||
|
|
||||||
|
export const zReservedFieldType = z.enum([
|
||||||
|
'WorkflowField',
|
||||||
|
'IsIntermediate',
|
||||||
|
'MetadataField',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type ReservedFieldType = z.infer<typeof zReservedFieldType>;
|
||||||
|
|
||||||
export const isFieldType = (value: unknown): value is FieldType =>
|
export const isFieldType = (value: unknown): value is FieldType =>
|
||||||
zFieldType.safeParse(value).success;
|
zFieldType.safeParse(value).success ||
|
||||||
|
zReservedFieldType.safeParse(value).success;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An input field template is generated on each page load from the OpenAPI schema.
|
* An input field template is generated on each page load from the OpenAPI schema.
|
||||||
@ -619,6 +628,11 @@ export type SchedulerInputFieldTemplate = InputFieldTemplateBase & {
|
|||||||
type: 'Scheduler';
|
type: 'Scheduler';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowInputFieldTemplate = InputFieldTemplateBase & {
|
||||||
|
default: undefined;
|
||||||
|
type: 'WorkflowField';
|
||||||
|
};
|
||||||
|
|
||||||
export const isInputFieldValue = (
|
export const isInputFieldValue = (
|
||||||
field?: InputFieldValue | OutputFieldValue
|
field?: InputFieldValue | OutputFieldValue
|
||||||
): field is InputFieldValue => Boolean(field && field.fieldKind === 'input');
|
): field is InputFieldValue => Boolean(field && field.fieldKind === 'input');
|
||||||
@ -715,6 +729,47 @@ export const isInvocationFieldSchema = (
|
|||||||
|
|
||||||
export type InvocationEdgeExtra = { type: 'default' | 'collapsed' };
|
export type InvocationEdgeExtra = { type: 'default' | 'collapsed' };
|
||||||
|
|
||||||
|
export const zCoreMetadata = z
|
||||||
|
.object({
|
||||||
|
app_version: z.string().nullish(),
|
||||||
|
generation_mode: z.string().nullish(),
|
||||||
|
positive_prompt: z.string().nullish(),
|
||||||
|
negative_prompt: z.string().nullish(),
|
||||||
|
width: z.number().int().nullish(),
|
||||||
|
height: z.number().int().nullish(),
|
||||||
|
seed: z.number().int().nullish(),
|
||||||
|
rand_device: z.string().nullish(),
|
||||||
|
cfg_scale: z.number().nullish(),
|
||||||
|
steps: z.number().int().nullish(),
|
||||||
|
scheduler: z.string().nullish(),
|
||||||
|
clip_skip: z.number().int().nullish(),
|
||||||
|
model: zMainOrOnnxModel.nullish(),
|
||||||
|
controlnets: z.array(zControlField).nullish(),
|
||||||
|
loras: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
lora: zLoRAModelField,
|
||||||
|
weight: z.number(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.nullish(),
|
||||||
|
vae: zVaeModelField.nullish(),
|
||||||
|
strength: z.number().nullish(),
|
||||||
|
init_image: z.string().nullish(),
|
||||||
|
positive_style_prompt: z.string().nullish(),
|
||||||
|
negative_style_prompt: z.string().nullish(),
|
||||||
|
refiner_model: zSDXLRefinerModel.nullish(),
|
||||||
|
refiner_cfg_scale: z.number().nullish(),
|
||||||
|
refiner_steps: z.number().int().nullish(),
|
||||||
|
refiner_scheduler: z.string().nullish(),
|
||||||
|
refiner_positive_aesthetic_store: z.number().nullish(),
|
||||||
|
refiner_negative_aesthetic_store: z.number().nullish(),
|
||||||
|
refiner_start: z.number().nullish(),
|
||||||
|
})
|
||||||
|
.catchall(z.record(z.any()));
|
||||||
|
|
||||||
|
export type CoreMetadata = z.infer<typeof zCoreMetadata>;
|
||||||
|
|
||||||
export const zInvocationNodeData = z.object({
|
export const zInvocationNodeData = z.object({
|
||||||
id: z.string().trim().min(1),
|
id: z.string().trim().min(1),
|
||||||
// no easy way to build this dynamically, and we don't want to anyways, because this will be used
|
// no easy way to build this dynamically, and we don't want to anyways, because this will be used
|
||||||
@ -725,6 +780,8 @@ export const zInvocationNodeData = z.object({
|
|||||||
label: z.string(),
|
label: z.string(),
|
||||||
isOpen: z.boolean(),
|
isOpen: z.boolean(),
|
||||||
notes: z.string(),
|
notes: z.string(),
|
||||||
|
embedWorkflow: z.boolean(),
|
||||||
|
isIntermediate: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Massage this to get better type safety while developing
|
// Massage this to get better type safety while developing
|
||||||
@ -817,10 +874,18 @@ export const zWorkflow = z.object({
|
|||||||
nodes: z.array(zWorkflowNode),
|
nodes: z.array(zWorkflowNode),
|
||||||
edges: z.array(zWorkflowEdge),
|
edges: z.array(zWorkflowEdge),
|
||||||
exposedFields: z.array(zFieldIdentifier),
|
exposedFields: z.array(zFieldIdentifier),
|
||||||
|
meta: z.object({
|
||||||
|
version: zSemVer,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Workflow = z.infer<typeof zWorkflow>;
|
export type Workflow = z.infer<typeof zWorkflow>;
|
||||||
|
|
||||||
|
export type ImageMetadataAndWorkflow = {
|
||||||
|
metadata?: CoreMetadata;
|
||||||
|
workflow?: Workflow;
|
||||||
|
};
|
||||||
|
|
||||||
export type CurrentImageNodeData = {
|
export type CurrentImageNodeData = {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'current_image';
|
type: 'current_image';
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { logger } from 'app/logging/logger';
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { NodesState } from '../store/types';
|
import { NodesState } from '../store/types';
|
||||||
import { Workflow, zWorkflowEdge, zWorkflowNode } from '../types/types';
|
import { Workflow, zWorkflowEdge, zWorkflowNode } from '../types/types';
|
||||||
|
import { fromZodError } from 'zod-validation-error';
|
||||||
|
import { parseify } from 'common/util/serialize';
|
||||||
|
|
||||||
export const buildWorkflow = (nodesState: NodesState): Workflow => {
|
export const buildWorkflow = (nodesState: NodesState): Workflow => {
|
||||||
const { workflow: workflowMeta, nodes, edges } = nodesState;
|
const { workflow: workflowMeta, nodes, edges } = nodesState;
|
||||||
@ -14,6 +15,10 @@ export const buildWorkflow = (nodesState: NodesState): Workflow => {
|
|||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
const result = zWorkflowNode.safeParse(node);
|
const result = zWorkflowNode.safeParse(node);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
const { message } = fromZodError(result.error, {
|
||||||
|
prefix: 'Unable to parse node',
|
||||||
|
});
|
||||||
|
logger('nodes').warn({ node: parseify(node) }, message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
workflow.nodes.push(result.data);
|
workflow.nodes.push(result.data);
|
||||||
@ -22,6 +27,10 @@ export const buildWorkflow = (nodesState: NodesState): Workflow => {
|
|||||||
edges.forEach((edge) => {
|
edges.forEach((edge) => {
|
||||||
const result = zWorkflowEdge.safeParse(edge);
|
const result = zWorkflowEdge.safeParse(edge);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
const { message } = fromZodError(result.error, {
|
||||||
|
prefix: 'Unable to parse edge',
|
||||||
|
});
|
||||||
|
logger('nodes').warn({ edge: parseify(edge) }, message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
workflow.edges.push(result.data);
|
workflow.edges.push(result.data);
|
||||||
@ -29,7 +38,3 @@ export const buildWorkflow = (nodesState: NodesState): Workflow => {
|
|||||||
|
|
||||||
return workflow;
|
return workflow;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const workflowSelector = createSelector(stateSelector, ({ nodes }) =>
|
|
||||||
buildWorkflow(nodes)
|
|
||||||
);
|
|
||||||
|
@ -27,7 +27,6 @@ import {
|
|||||||
UNetInputFieldTemplate,
|
UNetInputFieldTemplate,
|
||||||
VaeInputFieldTemplate,
|
VaeInputFieldTemplate,
|
||||||
VaeModelInputFieldTemplate,
|
VaeModelInputFieldTemplate,
|
||||||
isFieldType,
|
|
||||||
} from '../types/types';
|
} from '../types/types';
|
||||||
|
|
||||||
export type BaseFieldProperties = 'name' | 'title' | 'description';
|
export type BaseFieldProperties = 'name' | 'title' | 'description';
|
||||||
@ -408,9 +407,7 @@ const buildSchedulerInputFieldTemplate = ({
|
|||||||
return template;
|
return template;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFieldType = (
|
export const getFieldType = (schemaObject: InvocationFieldSchema): string => {
|
||||||
schemaObject: InvocationFieldSchema
|
|
||||||
): FieldType => {
|
|
||||||
let fieldType = '';
|
let fieldType = '';
|
||||||
|
|
||||||
const { ui_type } = schemaObject;
|
const { ui_type } = schemaObject;
|
||||||
@ -446,10 +443,6 @@ export const getFieldType = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFieldType(fieldType)) {
|
|
||||||
throw `Field type "${fieldType}" is unknown!`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fieldType;
|
return fieldType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -461,12 +454,9 @@ export const getFieldType = (
|
|||||||
export const buildInputFieldTemplate = (
|
export const buildInputFieldTemplate = (
|
||||||
nodeSchema: InvocationSchemaObject,
|
nodeSchema: InvocationSchemaObject,
|
||||||
fieldSchema: InvocationFieldSchema,
|
fieldSchema: InvocationFieldSchema,
|
||||||
name: string
|
name: string,
|
||||||
|
fieldType: FieldType
|
||||||
) => {
|
) => {
|
||||||
// console.log('input', schemaObject);
|
|
||||||
const fieldType = getFieldType(fieldSchema);
|
|
||||||
// console.log('input fieldType', fieldType);
|
|
||||||
|
|
||||||
const { input, ui_hidden, ui_component, ui_type, ui_order } = fieldSchema;
|
const { input, ui_hidden, ui_component, ui_type, ui_order } = fieldSchema;
|
||||||
|
|
||||||
const extra = {
|
const extra = {
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import * as png from '@stevebel/png';
|
||||||
|
import { logger } from 'app/logging/logger';
|
||||||
|
import {
|
||||||
|
ImageMetadataAndWorkflow,
|
||||||
|
zCoreMetadata,
|
||||||
|
zWorkflow,
|
||||||
|
} from 'features/nodes/types/types';
|
||||||
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
|
export const getMetadataAndWorkflowFromImageBlob = async (
|
||||||
|
image: Blob
|
||||||
|
): Promise<ImageMetadataAndWorkflow> => {
|
||||||
|
const data: ImageMetadataAndWorkflow = {};
|
||||||
|
try {
|
||||||
|
const buffer = await image.arrayBuffer();
|
||||||
|
const text = png.decode(buffer).text;
|
||||||
|
const rawMetadata = get(text, 'invokeai_metadata');
|
||||||
|
const rawWorkflow = get(text, 'invokeai_workflow');
|
||||||
|
if (rawMetadata) {
|
||||||
|
try {
|
||||||
|
data.metadata = zCoreMetadata.parse(JSON.parse(rawMetadata));
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rawWorkflow) {
|
||||||
|
try {
|
||||||
|
data.workflow = zWorkflow.parse(JSON.parse(rawWorkflow));
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger('nodes').warn('Unable to parse image');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
@ -4,6 +4,7 @@ import { cloneDeep, omit, reduce } from 'lodash-es';
|
|||||||
import { Graph } from 'services/api/types';
|
import { Graph } from 'services/api/types';
|
||||||
import { AnyInvocation } from 'services/events/types';
|
import { AnyInvocation } from 'services/events/types';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { buildWorkflow } from '../buildWorkflow';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We need to do special handling for some fields
|
* We need to do special handling for some fields
|
||||||
@ -34,12 +35,13 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => {
|
|||||||
const { nodes, edges } = nodesState;
|
const { nodes, edges } = nodesState;
|
||||||
|
|
||||||
const filteredNodes = nodes.filter(isInvocationNode);
|
const filteredNodes = nodes.filter(isInvocationNode);
|
||||||
|
const workflowJSON = JSON.stringify(buildWorkflow(nodesState));
|
||||||
|
|
||||||
// Reduce the node editor nodes into invocation graph nodes
|
// Reduce the node editor nodes into invocation graph nodes
|
||||||
const parsedNodes = filteredNodes.reduce<NonNullable<Graph['nodes']>>(
|
const parsedNodes = filteredNodes.reduce<NonNullable<Graph['nodes']>>(
|
||||||
(nodesAccumulator, node) => {
|
(nodesAccumulator, node) => {
|
||||||
const { id, data } = node;
|
const { id, data } = node;
|
||||||
const { type, inputs } = data;
|
const { type, inputs, isIntermediate, embedWorkflow } = data;
|
||||||
|
|
||||||
// Transform each node's inputs to simple key-value pairs
|
// Transform each node's inputs to simple key-value pairs
|
||||||
const transformedInputs = reduce(
|
const transformedInputs = reduce(
|
||||||
@ -58,8 +60,14 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => {
|
|||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
...transformedInputs,
|
...transformedInputs,
|
||||||
|
is_intermediate: isIntermediate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (embedWorkflow) {
|
||||||
|
// add the workflow to the node
|
||||||
|
Object.assign(graphNode, { workflow: workflowJSON });
|
||||||
|
}
|
||||||
|
|
||||||
// Add it to the nodes object
|
// Add it to the nodes object
|
||||||
Object.assign(nodesAccumulator, {
|
Object.assign(nodesAccumulator, {
|
||||||
[id]: graphNode,
|
[id]: graphNode,
|
||||||
|
@ -4,10 +4,12 @@ import { reduce } from 'lodash-es';
|
|||||||
import { OpenAPIV3 } from 'openapi-types';
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
import { AnyInvocationType } from 'services/events/types';
|
import { AnyInvocationType } from 'services/events/types';
|
||||||
import {
|
import {
|
||||||
|
FieldType,
|
||||||
InputFieldTemplate,
|
InputFieldTemplate,
|
||||||
InvocationSchemaObject,
|
InvocationSchemaObject,
|
||||||
InvocationTemplate,
|
InvocationTemplate,
|
||||||
OutputFieldTemplate,
|
OutputFieldTemplate,
|
||||||
|
isFieldType,
|
||||||
isInvocationFieldSchema,
|
isInvocationFieldSchema,
|
||||||
isInvocationOutputSchemaObject,
|
isInvocationOutputSchemaObject,
|
||||||
isInvocationSchemaObject,
|
isInvocationSchemaObject,
|
||||||
@ -16,23 +18,35 @@ import { buildInputFieldTemplate, getFieldType } from './fieldTemplateBuilders';
|
|||||||
|
|
||||||
const RESERVED_INPUT_FIELD_NAMES = ['id', 'type', 'metadata'];
|
const RESERVED_INPUT_FIELD_NAMES = ['id', 'type', 'metadata'];
|
||||||
const RESERVED_OUTPUT_FIELD_NAMES = ['type'];
|
const RESERVED_OUTPUT_FIELD_NAMES = ['type'];
|
||||||
|
const RESERVED_FIELD_TYPES = [
|
||||||
|
'WorkflowField',
|
||||||
|
'MetadataField',
|
||||||
|
'IsIntermediate',
|
||||||
|
];
|
||||||
|
|
||||||
const invocationDenylist: AnyInvocationType[] = [
|
const invocationDenylist: AnyInvocationType[] = [
|
||||||
'graph',
|
'graph',
|
||||||
'metadata_accumulator',
|
'metadata_accumulator',
|
||||||
];
|
];
|
||||||
|
|
||||||
const isAllowedInputField = (nodeType: string, fieldName: string) => {
|
const isReservedInputField = (nodeType: string, fieldName: string) => {
|
||||||
if (RESERVED_INPUT_FIELD_NAMES.includes(fieldName)) {
|
if (RESERVED_INPUT_FIELD_NAMES.includes(fieldName)) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
if (nodeType === 'collect' && fieldName === 'collection') {
|
if (nodeType === 'collect' && fieldName === 'collection') {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
if (nodeType === 'iterate' && fieldName === 'index') {
|
if (nodeType === 'iterate' && fieldName === 'index') {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isReservedFieldType = (fieldType: FieldType) => {
|
||||||
|
if (RESERVED_FIELD_TYPES.includes(fieldType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAllowedOutputField = (nodeType: string, fieldName: string) => {
|
const isAllowedOutputField = (nodeType: string, fieldName: string) => {
|
||||||
@ -62,10 +76,14 @@ export const parseSchema = (
|
|||||||
|
|
||||||
const inputs = reduce(
|
const inputs = reduce(
|
||||||
schema.properties,
|
schema.properties,
|
||||||
(inputsAccumulator, property, propertyName) => {
|
(
|
||||||
if (!isAllowedInputField(type, propertyName)) {
|
inputsAccumulator: Record<string, InputFieldTemplate>,
|
||||||
|
property,
|
||||||
|
propertyName
|
||||||
|
) => {
|
||||||
|
if (isReservedInputField(type, propertyName)) {
|
||||||
logger('nodes').trace(
|
logger('nodes').trace(
|
||||||
{ type, propertyName, property: parseify(property) },
|
{ node: type, fieldName: propertyName, field: parseify(property) },
|
||||||
'Skipped reserved input field'
|
'Skipped reserved input field'
|
||||||
);
|
);
|
||||||
return inputsAccumulator;
|
return inputsAccumulator;
|
||||||
@ -73,21 +91,64 @@ export const parseSchema = (
|
|||||||
|
|
||||||
if (!isInvocationFieldSchema(property)) {
|
if (!isInvocationFieldSchema(property)) {
|
||||||
logger('nodes').warn(
|
logger('nodes').warn(
|
||||||
{ type, propertyName, property: parseify(property) },
|
{ node: type, propertyName, property: parseify(property) },
|
||||||
'Unhandled input property'
|
'Unhandled input property'
|
||||||
);
|
);
|
||||||
return inputsAccumulator;
|
return inputsAccumulator;
|
||||||
}
|
}
|
||||||
|
|
||||||
const field = buildInputFieldTemplate(schema, property, propertyName);
|
const fieldType = getFieldType(property);
|
||||||
|
|
||||||
if (field) {
|
if (!isFieldType(fieldType)) {
|
||||||
inputsAccumulator[propertyName] = field;
|
logger('nodes').warn(
|
||||||
|
{
|
||||||
|
node: type,
|
||||||
|
fieldName: propertyName,
|
||||||
|
fieldType,
|
||||||
|
field: parseify(property),
|
||||||
|
},
|
||||||
|
'Skipping unknown input field type'
|
||||||
|
);
|
||||||
|
return inputsAccumulator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isReservedFieldType(fieldType)) {
|
||||||
|
logger('nodes').trace(
|
||||||
|
{
|
||||||
|
node: type,
|
||||||
|
fieldName: propertyName,
|
||||||
|
fieldType,
|
||||||
|
field: parseify(property),
|
||||||
|
},
|
||||||
|
'Skipping reserved field type'
|
||||||
|
);
|
||||||
|
return inputsAccumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = buildInputFieldTemplate(
|
||||||
|
schema,
|
||||||
|
property,
|
||||||
|
propertyName,
|
||||||
|
fieldType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!field) {
|
||||||
|
logger('nodes').warn(
|
||||||
|
{
|
||||||
|
node: type,
|
||||||
|
fieldName: propertyName,
|
||||||
|
fieldType,
|
||||||
|
field: parseify(property),
|
||||||
|
},
|
||||||
|
'Skipping input field with no template'
|
||||||
|
);
|
||||||
|
return inputsAccumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputsAccumulator[propertyName] = field;
|
||||||
return inputsAccumulator;
|
return inputsAccumulator;
|
||||||
},
|
},
|
||||||
{} as Record<string, InputFieldTemplate>
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
const outputSchemaName = schema.output.$ref.split('/').pop();
|
const outputSchemaName = schema.output.$ref.split('/').pop();
|
||||||
@ -136,16 +197,24 @@ export const parseSchema = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fieldType = getFieldType(property);
|
const fieldType = getFieldType(property);
|
||||||
outputsAccumulator[propertyName] = {
|
|
||||||
fieldKind: 'output',
|
if (!isFieldType(fieldType)) {
|
||||||
name: propertyName,
|
logger('nodes').warn(
|
||||||
title: property.title ?? '',
|
{ fieldName: propertyName, fieldType, field: parseify(property) },
|
||||||
description: property.description ?? '',
|
'Skipping unknown output field type'
|
||||||
type: fieldType,
|
);
|
||||||
ui_hidden: property.ui_hidden ?? false,
|
} else {
|
||||||
ui_type: property.ui_type,
|
outputsAccumulator[propertyName] = {
|
||||||
ui_order: property.ui_order,
|
fieldKind: 'output',
|
||||||
};
|
name: propertyName,
|
||||||
|
title: property.title ?? '',
|
||||||
|
description: property.description ?? '',
|
||||||
|
type: fieldType,
|
||||||
|
ui_hidden: property.ui_hidden ?? false,
|
||||||
|
ui_type: property.ui_type,
|
||||||
|
ui_order: property.ui_order,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return outputsAccumulator;
|
return outputsAccumulator;
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { CoreMetadata } from 'features/nodes/types/types';
|
||||||
import {
|
import {
|
||||||
refinerModelChanged,
|
refinerModelChanged,
|
||||||
setNegativeStylePromptSDXL,
|
setNegativeStylePromptSDXL,
|
||||||
@ -13,7 +14,7 @@ import {
|
|||||||
} from 'features/sdxl/store/sdxlSlice';
|
} from 'features/sdxl/store/sdxlSlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ImageDTO, UnsafeImageMetadata } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { initialImageSelected, modelSelected } from '../store/actions';
|
import { initialImageSelected, modelSelected } from '../store/actions';
|
||||||
import {
|
import {
|
||||||
setCfgScale,
|
setCfgScale,
|
||||||
@ -317,7 +318,7 @@ export const useRecallParameters = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const recallAllParameters = useCallback(
|
const recallAllParameters = useCallback(
|
||||||
(metadata: UnsafeImageMetadata['metadata'] | undefined) => {
|
(metadata: CoreMetadata | undefined) => {
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
allParameterNotSetToast();
|
allParameterNotSetToast();
|
||||||
return;
|
return;
|
||||||
|
@ -29,13 +29,15 @@ export const $projectId = atom<string | undefined>();
|
|||||||
* @example
|
* @example
|
||||||
* const { get, post, del } = $client.get();
|
* const { get, post, del } = $client.get();
|
||||||
*/
|
*/
|
||||||
export const $client = computed([$authToken, $baseUrl, $projectId], (authToken, baseUrl, projectId) =>
|
export const $client = computed(
|
||||||
createClient<paths>({
|
[$authToken, $baseUrl, $projectId],
|
||||||
headers: {
|
(authToken, baseUrl, projectId) =>
|
||||||
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
createClient<paths>({
|
||||||
...(projectId ? { "project-id": projectId } : {})
|
headers: {
|
||||||
},
|
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
||||||
// do not include `api/v1` in the base url for this client
|
...(projectId ? { 'project-id': projectId } : {}),
|
||||||
baseUrl: `${baseUrl ?? ''}`,
|
},
|
||||||
})
|
// do not include `api/v1` in the base url for this client
|
||||||
|
baseUrl: `${baseUrl ?? ''}`,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
@ -19,7 +19,7 @@ export const boardsApi = api.injectEndpoints({
|
|||||||
*/
|
*/
|
||||||
listBoards: build.query<OffsetPaginatedResults_BoardDTO_, ListBoardsArg>({
|
listBoards: build.query<OffsetPaginatedResults_BoardDTO_, ListBoardsArg>({
|
||||||
query: (arg) => ({ url: 'boards/', params: arg }),
|
query: (arg) => ({ url: 'boards/', params: arg }),
|
||||||
providesTags: (result, error, arg) => {
|
providesTags: (result) => {
|
||||||
// any list of boards
|
// any list of boards
|
||||||
const tags: ApiFullTagDescription[] = [{ type: 'Board', id: LIST_TAG }];
|
const tags: ApiFullTagDescription[] = [{ type: 'Board', id: LIST_TAG }];
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ export const boardsApi = api.injectEndpoints({
|
|||||||
url: 'boards/',
|
url: 'boards/',
|
||||||
params: { all: true },
|
params: { all: true },
|
||||||
}),
|
}),
|
||||||
providesTags: (result, error, arg) => {
|
providesTags: (result) => {
|
||||||
// any list of boards
|
// any list of boards
|
||||||
const tags: ApiFullTagDescription[] = [{ type: 'Board', id: LIST_TAG }];
|
const tags: ApiFullTagDescription[] = [{ type: 'Board', id: LIST_TAG }];
|
||||||
|
|
||||||
|
@ -6,7 +6,8 @@ import {
|
|||||||
IMAGE_CATEGORIES,
|
IMAGE_CATEGORIES,
|
||||||
IMAGE_LIMIT,
|
IMAGE_LIMIT,
|
||||||
} from 'features/gallery/store/types';
|
} from 'features/gallery/store/types';
|
||||||
import { keyBy } from 'lodash';
|
import { getMetadataAndWorkflowFromImageBlob } from 'features/nodes/util/getMetadataAndWorkflowFromImageBlob';
|
||||||
|
import { keyBy } from 'lodash-es';
|
||||||
import { ApiFullTagDescription, LIST_TAG, api } from '..';
|
import { ApiFullTagDescription, LIST_TAG, api } from '..';
|
||||||
import { components, paths } from '../schema';
|
import { components, paths } from '../schema';
|
||||||
import {
|
import {
|
||||||
@ -26,6 +27,7 @@ import {
|
|||||||
imagesSelectors,
|
imagesSelectors,
|
||||||
} from '../util';
|
} from '../util';
|
||||||
import { boardsApi } from './boards';
|
import { boardsApi } from './boards';
|
||||||
|
import { ImageMetadataAndWorkflow } from 'features/nodes/types/types';
|
||||||
|
|
||||||
export const imagesApi = api.injectEndpoints({
|
export const imagesApi = api.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
@ -113,6 +115,19 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
],
|
],
|
||||||
keepUnusedDataFor: 86400, // 24 hours
|
keepUnusedDataFor: 86400, // 24 hours
|
||||||
}),
|
}),
|
||||||
|
getImageMetadataFromFile: build.query<ImageMetadataAndWorkflow, string>({
|
||||||
|
query: (image_name) => ({
|
||||||
|
url: `images/i/${image_name}/full`,
|
||||||
|
responseHandler: async (res) => {
|
||||||
|
return await res.blob();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
providesTags: (result, error, image_name) => [
|
||||||
|
{ type: 'ImageMetadataFromFile', id: image_name },
|
||||||
|
],
|
||||||
|
transformResponse: (response: Blob) =>
|
||||||
|
getMetadataAndWorkflowFromImageBlob(response),
|
||||||
|
}),
|
||||||
clearIntermediates: build.mutation<number, void>({
|
clearIntermediates: build.mutation<number, void>({
|
||||||
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
||||||
invalidatesTags: ['IntermediatesCount'],
|
invalidatesTags: ['IntermediatesCount'],
|
||||||
@ -357,7 +372,7 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
],
|
],
|
||||||
async onQueryStarted(
|
async onQueryStarted(
|
||||||
{ imageDTO, session_id },
|
{ imageDTO, session_id },
|
||||||
{ dispatch, queryFulfilled, getState }
|
{ dispatch, queryFulfilled }
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Cache changes for `changeImageSessionId`:
|
* Cache changes for `changeImageSessionId`:
|
||||||
@ -432,7 +447,9 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
data.updated_image_names.includes(i.image_name)
|
data.updated_image_names.includes(i.image_name)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!updatedImages[0]) return;
|
if (!updatedImages[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// assume all images are on the same board/category
|
// assume all images are on the same board/category
|
||||||
const categories = getCategories(updatedImages[0]);
|
const categories = getCategories(updatedImages[0]);
|
||||||
@ -544,7 +561,9 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
data.updated_image_names.includes(i.image_name)
|
data.updated_image_names.includes(i.image_name)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!updatedImages[0]) return;
|
if (!updatedImages[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// assume all images are on the same board/category
|
// assume all images are on the same board/category
|
||||||
const categories = getCategories(updatedImages[0]);
|
const categories = getCategories(updatedImages[0]);
|
||||||
const boardId = updatedImages[0].board_id;
|
const boardId = updatedImages[0].board_id;
|
||||||
@ -645,17 +664,7 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async onQueryStarted(
|
async onQueryStarted(_, { dispatch, queryFulfilled }) {
|
||||||
{
|
|
||||||
file,
|
|
||||||
image_category,
|
|
||||||
is_intermediate,
|
|
||||||
postUploadAction,
|
|
||||||
session_id,
|
|
||||||
board_id,
|
|
||||||
},
|
|
||||||
{ dispatch, queryFulfilled }
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
/**
|
/**
|
||||||
* NOTE: PESSIMISTIC UPDATE
|
* NOTE: PESSIMISTIC UPDATE
|
||||||
@ -712,7 +721,7 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
|
|
||||||
deleteBoard: build.mutation<DeleteBoardResult, string>({
|
deleteBoard: build.mutation<DeleteBoardResult, string>({
|
||||||
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
|
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
|
||||||
invalidatesTags: (result, error, board_id) => [
|
invalidatesTags: () => [
|
||||||
{ type: 'Board', id: LIST_TAG },
|
{ type: 'Board', id: LIST_TAG },
|
||||||
// invalidate the 'No Board' cache
|
// invalidate the 'No Board' cache
|
||||||
{
|
{
|
||||||
@ -732,7 +741,7 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
{ type: 'BoardImagesTotal', id: 'none' },
|
{ type: 'BoardImagesTotal', id: 'none' },
|
||||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
async onQueryStarted(board_id, { dispatch, queryFulfilled }) {
|
||||||
/**
|
/**
|
||||||
* Cache changes for deleteBoard:
|
* Cache changes for deleteBoard:
|
||||||
* - Update every image in the 'getImageDTO' cache that has the board_id
|
* - Update every image in the 'getImageDTO' cache that has the board_id
|
||||||
@ -802,7 +811,7 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
params: { include_images: true },
|
params: { include_images: true },
|
||||||
}),
|
}),
|
||||||
invalidatesTags: (result, error, board_id) => [
|
invalidatesTags: () => [
|
||||||
{ type: 'Board', id: LIST_TAG },
|
{ type: 'Board', id: LIST_TAG },
|
||||||
{
|
{
|
||||||
type: 'ImageList',
|
type: 'ImageList',
|
||||||
@ -821,7 +830,7 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
{ type: 'BoardImagesTotal', id: 'none' },
|
{ type: 'BoardImagesTotal', id: 'none' },
|
||||||
{ type: 'BoardAssetsTotal', id: 'none' },
|
{ type: 'BoardAssetsTotal', id: 'none' },
|
||||||
],
|
],
|
||||||
async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
|
async onQueryStarted(board_id, { dispatch, queryFulfilled }) {
|
||||||
/**
|
/**
|
||||||
* Cache changes for deleteBoardAndImages:
|
* Cache changes for deleteBoardAndImages:
|
||||||
* - ~~Remove every image in the 'getImageDTO' cache that has the board_id~~
|
* - ~~Remove every image in the 'getImageDTO' cache that has the board_id~~
|
||||||
@ -1253,9 +1262,8 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
];
|
];
|
||||||
|
|
||||||
result?.removed_image_names.forEach((image_name) => {
|
result?.removed_image_names.forEach((image_name) => {
|
||||||
const board_id = imageDTOs.find(
|
const board_id = imageDTOs.find((i) => i.image_name === image_name)
|
||||||
(i) => i.image_name === image_name
|
?.board_id;
|
||||||
)?.board_id;
|
|
||||||
|
|
||||||
if (!board_id || touchedBoardIds.includes(board_id)) {
|
if (!board_id || touchedBoardIds.includes(board_id)) {
|
||||||
return;
|
return;
|
||||||
@ -1385,4 +1393,5 @@ export const {
|
|||||||
useDeleteBoardMutation,
|
useDeleteBoardMutation,
|
||||||
useStarImagesMutation,
|
useStarImagesMutation,
|
||||||
useUnstarImagesMutation,
|
useUnstarImagesMutation,
|
||||||
|
useGetImageMetadataFromFileQuery,
|
||||||
} = imagesApi;
|
} = imagesApi;
|
||||||
|
@ -178,7 +178,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
const query = queryString.stringify(params, { arrayFormat: 'none' });
|
const query = queryString.stringify(params, { arrayFormat: 'none' });
|
||||||
return `models/?${query}`;
|
return `models/?${query}`;
|
||||||
},
|
},
|
||||||
providesTags: (result, error, arg) => {
|
providesTags: (result) => {
|
||||||
const tags: ApiFullTagDescription[] = [
|
const tags: ApiFullTagDescription[] = [
|
||||||
{ type: 'OnnxModel', id: LIST_TAG },
|
{ type: 'OnnxModel', id: LIST_TAG },
|
||||||
];
|
];
|
||||||
@ -194,11 +194,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
},
|
},
|
||||||
transformResponse: (
|
transformResponse: (response: { models: OnnxModelConfig[] }) => {
|
||||||
response: { models: OnnxModelConfig[] },
|
|
||||||
meta,
|
|
||||||
arg
|
|
||||||
) => {
|
|
||||||
const entities = createModelEntities<OnnxModelConfigEntity>(
|
const entities = createModelEntities<OnnxModelConfigEntity>(
|
||||||
response.models
|
response.models
|
||||||
);
|
);
|
||||||
@ -221,7 +217,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
const query = queryString.stringify(params, { arrayFormat: 'none' });
|
const query = queryString.stringify(params, { arrayFormat: 'none' });
|
||||||
return `models/?${query}`;
|
return `models/?${query}`;
|
||||||
},
|
},
|
||||||
providesTags: (result, error, arg) => {
|
providesTags: (result) => {
|
||||||
const tags: ApiFullTagDescription[] = [
|
const tags: ApiFullTagDescription[] = [
|
||||||
{ type: 'MainModel', id: LIST_TAG },
|
{ type: 'MainModel', id: LIST_TAG },
|
||||||
];
|
];
|
||||||
@ -237,11 +233,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
},
|
},
|
||||||
transformResponse: (
|
transformResponse: (response: { models: MainModelConfig[] }) => {
|
||||||
response: { models: MainModelConfig[] },
|
|
||||||
meta,
|
|
||||||
arg
|
|
||||||
) => {
|
|
||||||
const entities = createModelEntities<MainModelConfigEntity>(
|
const entities = createModelEntities<MainModelConfigEntity>(
|
||||||
response.models
|
response.models
|
||||||
);
|
);
|
||||||
@ -361,7 +353,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
getLoRAModels: build.query<EntityState<LoRAModelConfigEntity>, void>({
|
getLoRAModels: build.query<EntityState<LoRAModelConfigEntity>, void>({
|
||||||
query: () => ({ url: 'models/', params: { model_type: 'lora' } }),
|
query: () => ({ url: 'models/', params: { model_type: 'lora' } }),
|
||||||
providesTags: (result, error, arg) => {
|
providesTags: (result) => {
|
||||||
const tags: ApiFullTagDescription[] = [
|
const tags: ApiFullTagDescription[] = [
|
||||||
{ type: 'LoRAModel', id: LIST_TAG },
|
{ type: 'LoRAModel', id: LIST_TAG },
|
||||||
];
|
];
|
||||||
@ -377,11 +369,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
},
|
},
|
||||||
transformResponse: (
|
transformResponse: (response: { models: LoRAModelConfig[] }) => {
|
||||||
response: { models: LoRAModelConfig[] },
|
|
||||||
meta,
|
|
||||||
arg
|
|
||||||
) => {
|
|
||||||
const entities = createModelEntities<LoRAModelConfigEntity>(
|
const entities = createModelEntities<LoRAModelConfigEntity>(
|
||||||
response.models
|
response.models
|
||||||
);
|
);
|
||||||
@ -421,7 +409,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
void
|
void
|
||||||
>({
|
>({
|
||||||
query: () => ({ url: 'models/', params: { model_type: 'controlnet' } }),
|
query: () => ({ url: 'models/', params: { model_type: 'controlnet' } }),
|
||||||
providesTags: (result, error, arg) => {
|
providesTags: (result) => {
|
||||||
const tags: ApiFullTagDescription[] = [
|
const tags: ApiFullTagDescription[] = [
|
||||||
{ type: 'ControlNetModel', id: LIST_TAG },
|
{ type: 'ControlNetModel', id: LIST_TAG },
|
||||||
];
|
];
|
||||||
@ -437,11 +425,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
},
|
},
|
||||||
transformResponse: (
|
transformResponse: (response: { models: ControlNetModelConfig[] }) => {
|
||||||
response: { models: ControlNetModelConfig[] },
|
|
||||||
meta,
|
|
||||||
arg
|
|
||||||
) => {
|
|
||||||
const entities = createModelEntities<ControlNetModelConfigEntity>(
|
const entities = createModelEntities<ControlNetModelConfigEntity>(
|
||||||
response.models
|
response.models
|
||||||
);
|
);
|
||||||
@ -453,7 +437,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
getVaeModels: build.query<EntityState<VaeModelConfigEntity>, void>({
|
getVaeModels: build.query<EntityState<VaeModelConfigEntity>, void>({
|
||||||
query: () => ({ url: 'models/', params: { model_type: 'vae' } }),
|
query: () => ({ url: 'models/', params: { model_type: 'vae' } }),
|
||||||
providesTags: (result, error, arg) => {
|
providesTags: (result) => {
|
||||||
const tags: ApiFullTagDescription[] = [
|
const tags: ApiFullTagDescription[] = [
|
||||||
{ type: 'VaeModel', id: LIST_TAG },
|
{ type: 'VaeModel', id: LIST_TAG },
|
||||||
];
|
];
|
||||||
@ -469,11 +453,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
},
|
},
|
||||||
transformResponse: (
|
transformResponse: (response: { models: VaeModelConfig[] }) => {
|
||||||
response: { models: VaeModelConfig[] },
|
|
||||||
meta,
|
|
||||||
arg
|
|
||||||
) => {
|
|
||||||
const entities = createModelEntities<VaeModelConfigEntity>(
|
const entities = createModelEntities<VaeModelConfigEntity>(
|
||||||
response.models
|
response.models
|
||||||
);
|
);
|
||||||
@ -488,7 +468,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
void
|
void
|
||||||
>({
|
>({
|
||||||
query: () => ({ url: 'models/', params: { model_type: 'embedding' } }),
|
query: () => ({ url: 'models/', params: { model_type: 'embedding' } }),
|
||||||
providesTags: (result, error, arg) => {
|
providesTags: (result) => {
|
||||||
const tags: ApiFullTagDescription[] = [
|
const tags: ApiFullTagDescription[] = [
|
||||||
{ type: 'TextualInversionModel', id: LIST_TAG },
|
{ type: 'TextualInversionModel', id: LIST_TAG },
|
||||||
];
|
];
|
||||||
@ -504,11 +484,9 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
},
|
},
|
||||||
transformResponse: (
|
transformResponse: (response: {
|
||||||
response: { models: TextualInversionModelConfig[] },
|
models: TextualInversionModelConfig[];
|
||||||
meta,
|
}) => {
|
||||||
arg
|
|
||||||
) => {
|
|
||||||
const entities = createModelEntities<TextualInversionModelConfigEntity>(
|
const entities = createModelEntities<TextualInversionModelConfigEntity>(
|
||||||
response.models
|
response.models
|
||||||
);
|
);
|
||||||
@ -525,7 +503,7 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
url: `/models/search?${folderQueryStr}`,
|
url: `/models/search?${folderQueryStr}`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
providesTags: (result, error, arg) => {
|
providesTags: (result) => {
|
||||||
const tags: ApiFullTagDescription[] = [
|
const tags: ApiFullTagDescription[] = [
|
||||||
{ type: 'ScannedModels', id: LIST_TAG },
|
{ type: 'ScannedModels', id: LIST_TAG },
|
||||||
];
|
];
|
||||||
|
@ -16,6 +16,7 @@ export const tagTypes = [
|
|||||||
'ImageNameList',
|
'ImageNameList',
|
||||||
'ImageList',
|
'ImageList',
|
||||||
'ImageMetadata',
|
'ImageMetadata',
|
||||||
|
'ImageMetadataFromFile',
|
||||||
'Model',
|
'Model',
|
||||||
];
|
];
|
||||||
export type ApiFullTagDescription = FullTagDescription<
|
export type ApiFullTagDescription = FullTagDescription<
|
||||||
@ -39,7 +40,7 @@ const dynamicBaseQuery: BaseQueryFn<
|
|||||||
headers.set('Authorization', `Bearer ${authToken}`);
|
headers.set('Authorization', `Bearer ${authToken}`);
|
||||||
}
|
}
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
headers.set("project-id", projectId)
|
headers.set('project-id', projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
|
510
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
510
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,16 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
function getCircularReplacer() {
|
function getCircularReplacer() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const ancestors: Record<string, any>[] = [];
|
const ancestors: Record<string, any>[] = [];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return function (key: string, value: any) {
|
return function (key: string, value: any) {
|
||||||
if (typeof value !== 'object' || value === null) {
|
if (typeof value !== 'object' || value === null) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
// `this` is the object that value is contained in,
|
// `this` is the object that value is contained in, i.e., its direct parent.
|
||||||
// i.e., its direct parent.
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore don't think it's possible to not have TS complain about this...
|
||||||
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
||||||
ancestors.pop();
|
ancestors.pop();
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ export const sessionInvoked = createAsyncThunk<
|
|||||||
>('api/sessionInvoked', async (arg, { rejectWithValue }) => {
|
>('api/sessionInvoked', async (arg, { rejectWithValue }) => {
|
||||||
const { session_id } = arg;
|
const { session_id } = arg;
|
||||||
const { PUT } = $client.get();
|
const { PUT } = $client.get();
|
||||||
const { data, error, response } = await PUT(
|
const { error, response } = await PUT(
|
||||||
'/api/v1/sessions/{session_id}/invoke',
|
'/api/v1/sessions/{session_id}/invoke',
|
||||||
{
|
{
|
||||||
params: { query: { all: true }, path: { session_id } },
|
params: { query: { all: true }, path: { session_id } },
|
||||||
@ -85,6 +85,7 @@ export const sessionInvoked = createAsyncThunk<
|
|||||||
return rejectWithValue({
|
return rejectWithValue({
|
||||||
arg,
|
arg,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
error: (error as any).body.detail,
|
error: (error as any).body.detail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -124,14 +125,11 @@ export const sessionCanceled = createAsyncThunk<
|
|||||||
>('api/sessionCanceled', async (arg, { rejectWithValue }) => {
|
>('api/sessionCanceled', async (arg, { rejectWithValue }) => {
|
||||||
const { session_id } = arg;
|
const { session_id } = arg;
|
||||||
const { DELETE } = $client.get();
|
const { DELETE } = $client.get();
|
||||||
const { data, error, response } = await DELETE(
|
const { data, error } = await DELETE('/api/v1/sessions/{session_id}/invoke', {
|
||||||
'/api/v1/sessions/{session_id}/invoke',
|
params: {
|
||||||
{
|
path: { session_id },
|
||||||
params: {
|
},
|
||||||
path: { session_id },
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return rejectWithValue({ arg, error });
|
return rejectWithValue({ arg, error });
|
||||||
@ -164,7 +162,7 @@ export const listedSessions = createAsyncThunk<
|
|||||||
>('api/listSessions', async (arg, { rejectWithValue }) => {
|
>('api/listSessions', async (arg, { rejectWithValue }) => {
|
||||||
const { params } = arg;
|
const { params } = arg;
|
||||||
const { GET } = $client.get();
|
const { GET } = $client.get();
|
||||||
const { data, error, response } = await GET('/api/v1/sessions/', {
|
const { data, error } = await GET('/api/v1/sessions/', {
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,15 +26,21 @@ export const getIsImageInDateRange = (
|
|||||||
|
|
||||||
for (let index = 0; index < totalCachedImageDtos.length; index++) {
|
for (let index = 0; index < totalCachedImageDtos.length; index++) {
|
||||||
const image = totalCachedImageDtos[index];
|
const image = totalCachedImageDtos[index];
|
||||||
if (image?.starred) cachedStarredImages.push(image);
|
if (image?.starred) {
|
||||||
if (!image?.starred) cachedUnstarredImages.push(image);
|
cachedStarredImages.push(image);
|
||||||
|
}
|
||||||
|
if (!image?.starred) {
|
||||||
|
cachedUnstarredImages.push(image);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageDTO.starred) {
|
if (imageDTO.starred) {
|
||||||
const lastStarredImage =
|
const lastStarredImage =
|
||||||
cachedStarredImages[cachedStarredImages.length - 1];
|
cachedStarredImages[cachedStarredImages.length - 1];
|
||||||
// if starring or already starred, want to look in list of starred images
|
// if starring or already starred, want to look in list of starred images
|
||||||
if (!lastStarredImage) return true; // no starred images showing, so always show this one
|
if (!lastStarredImage) {
|
||||||
|
return true;
|
||||||
|
} // no starred images showing, so always show this one
|
||||||
const createdDate = new Date(imageDTO.created_at);
|
const createdDate = new Date(imageDTO.created_at);
|
||||||
const oldestDate = new Date(lastStarredImage.created_at);
|
const oldestDate = new Date(lastStarredImage.created_at);
|
||||||
return createdDate >= oldestDate;
|
return createdDate >= oldestDate;
|
||||||
@ -42,7 +48,9 @@ export const getIsImageInDateRange = (
|
|||||||
const lastUnstarredImage =
|
const lastUnstarredImage =
|
||||||
cachedUnstarredImages[cachedUnstarredImages.length - 1];
|
cachedUnstarredImages[cachedUnstarredImages.length - 1];
|
||||||
// if unstarring or already unstarred, want to look in list of unstarred images
|
// if unstarring or already unstarred, want to look in list of unstarred images
|
||||||
if (!lastUnstarredImage) return false; // no unstarred images showing, so don't show this one
|
if (!lastUnstarredImage) {
|
||||||
|
return false;
|
||||||
|
} // no unstarred images showing, so don't show this one
|
||||||
const createdDate = new Date(imageDTO.created_at);
|
const createdDate = new Date(imageDTO.created_at);
|
||||||
const oldestDate = new Date(lastUnstarredImage.created_at);
|
const oldestDate = new Date(lastUnstarredImage.created_at);
|
||||||
return createdDate >= oldestDate;
|
return createdDate >= oldestDate;
|
||||||
|
@ -1727,6 +1727,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
|
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
|
||||||
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
|
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
|
||||||
|
|
||||||
|
"@stevebel/png@^1.5.1":
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@stevebel/png/-/png-1.5.1.tgz#c1179a2787c7440fc20082d6eff85362450f24f6"
|
||||||
|
integrity sha512-cUVgrRCgOQLqLpXvV4HffvkITWF1BBgslXkINKfMD2b+GkAbV+PeO6IeMF6k7c6FLvGox6mMLwwqcXKoDha9rw==
|
||||||
|
dependencies:
|
||||||
|
pako "^2.1.0"
|
||||||
|
|
||||||
"@swc/core-darwin-arm64@1.3.70":
|
"@swc/core-darwin-arm64@1.3.70":
|
||||||
version "1.3.70"
|
version "1.3.70"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.70.tgz#056ac6899e22cb7f7be21388d4d938ca5123a72b"
|
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.70.tgz#056ac6899e22cb7f7be21388d4d938ca5123a72b"
|
||||||
@ -5372,6 +5379,11 @@ p-locate@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit "^3.0.2"
|
p-limit "^3.0.2"
|
||||||
|
|
||||||
|
pako@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
|
||||||
|
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
|
||||||
|
|
||||||
parent-module@^1.0.0:
|
parent-module@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user