mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into lstein/feat/simple-mm2-api
This commit is contained in:
commit
987ee704a1
@ -98,7 +98,7 @@ Updating is exactly the same as installing - download the latest installer, choo
|
|||||||
|
|
||||||
If you have installation issues, please review the [FAQ]. You can also [create an issue] or ask for help on [discord].
|
If you have installation issues, please review the [FAQ]. You can also [create an issue] or ask for help on [discord].
|
||||||
|
|
||||||
[installation requirements]: INSTALLATION.md#installation-requirements
|
[installation requirements]: INSTALL_REQUIREMENTS.md
|
||||||
[FAQ]: ../help/FAQ.md
|
[FAQ]: ../help/FAQ.md
|
||||||
[install some models]: 050_INSTALLING_MODELS.md
|
[install some models]: 050_INSTALLING_MODELS.md
|
||||||
[configuration docs]: ../features/CONFIGURATION.md
|
[configuration docs]: ../features/CONFIGURATION.md
|
||||||
|
@ -37,13 +37,13 @@ Invoke runs best with a dedicated GPU, but will fall back to running on CPU, alb
|
|||||||
=== "Nvidia"
|
=== "Nvidia"
|
||||||
|
|
||||||
```
|
```
|
||||||
Any GPU with at least 8GB VRAM. Linux only.
|
Any GPU with at least 8GB VRAM.
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "AMD"
|
=== "AMD"
|
||||||
|
|
||||||
```
|
```
|
||||||
Any GPU with at least 16GB VRAM.
|
Any GPU with at least 16GB VRAM. Linux only.
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "Mac"
|
=== "Mac"
|
||||||
|
@ -13,7 +13,6 @@ from pydantic import BaseModel, Field
|
|||||||
from invokeai.app.invocations.upscale import ESRGAN_MODELS
|
from invokeai.app.invocations.upscale import ESRGAN_MODELS
|
||||||
from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
|
from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
|
||||||
from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch
|
from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch
|
||||||
from invokeai.backend.image_util.safety_checker import SafetyChecker
|
|
||||||
from invokeai.backend.util.logging import logging
|
from invokeai.backend.util.logging import logging
|
||||||
from invokeai.version import __version__
|
from invokeai.version import __version__
|
||||||
|
|
||||||
@ -109,9 +108,7 @@ async def get_config() -> AppConfig:
|
|||||||
upscaling_models.append(str(Path(model).stem))
|
upscaling_models.append(str(Path(model).stem))
|
||||||
upscaler = Upscaler(upscaling_method="esrgan", upscaling_models=upscaling_models)
|
upscaler = Upscaler(upscaling_method="esrgan", upscaling_models=upscaling_models)
|
||||||
|
|
||||||
nsfw_methods = []
|
nsfw_methods = ["nsfw_checker"]
|
||||||
if SafetyChecker.safety_checker_available():
|
|
||||||
nsfw_methods.append("nsfw_checker")
|
|
||||||
|
|
||||||
watermarking_methods = ["invisible_watermark"]
|
watermarking_methods = ["invisible_watermark"]
|
||||||
|
|
||||||
|
@ -6,13 +6,12 @@ from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request,
|
|||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from pydantic import BaseModel, Field, ValidationError
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator
|
from invokeai.app.invocations.fields import MetadataField
|
||||||
from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin
|
from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin
|
||||||
from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO
|
from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO
|
||||||
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
|
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
|
||||||
from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID, WorkflowWithoutIDValidator
|
|
||||||
|
|
||||||
from ..dependencies import ApiDependencies
|
from ..dependencies import ApiDependencies
|
||||||
|
|
||||||
@ -49,6 +48,7 @@ async def upload_image(
|
|||||||
|
|
||||||
metadata = None
|
metadata = None
|
||||||
workflow = None
|
workflow = None
|
||||||
|
graph = None
|
||||||
|
|
||||||
contents = await file.read()
|
contents = await file.read()
|
||||||
try:
|
try:
|
||||||
@ -63,21 +63,27 @@ async def upload_image(
|
|||||||
# TODO: retain non-invokeai metadata on upload?
|
# TODO: retain non-invokeai metadata on upload?
|
||||||
# attempt to parse metadata from image
|
# attempt to parse metadata from image
|
||||||
metadata_raw = pil_image.info.get("invokeai_metadata", None)
|
metadata_raw = pil_image.info.get("invokeai_metadata", None)
|
||||||
if metadata_raw:
|
if isinstance(metadata_raw, str):
|
||||||
try:
|
metadata = metadata_raw
|
||||||
metadata = MetadataFieldValidator.validate_json(metadata_raw)
|
else:
|
||||||
except ValidationError:
|
ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image")
|
||||||
ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image")
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
# attempt to parse workflow from image
|
# attempt to parse workflow from image
|
||||||
workflow_raw = pil_image.info.get("invokeai_workflow", None)
|
workflow_raw = pil_image.info.get("invokeai_workflow", None)
|
||||||
if workflow_raw is not None:
|
if isinstance(workflow_raw, str):
|
||||||
try:
|
workflow = workflow_raw
|
||||||
workflow = WorkflowWithoutIDValidator.validate_json(workflow_raw)
|
else:
|
||||||
except ValidationError:
|
ApiDependencies.invoker.services.logger.warn("Failed to parse workflow for uploaded image")
|
||||||
ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image")
|
pass
|
||||||
pass
|
|
||||||
|
# attempt to extract graph from image
|
||||||
|
graph_raw = pil_image.info.get("invokeai_graph", None)
|
||||||
|
if isinstance(graph_raw, str):
|
||||||
|
graph = graph_raw
|
||||||
|
else:
|
||||||
|
ApiDependencies.invoker.services.logger.warn("Failed to parse graph for uploaded image")
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image_dto = ApiDependencies.invoker.services.images.create(
|
image_dto = ApiDependencies.invoker.services.images.create(
|
||||||
@ -88,6 +94,7 @@ async def upload_image(
|
|||||||
board_id=board_id,
|
board_id=board_id,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
workflow=workflow,
|
workflow=workflow,
|
||||||
|
graph=graph,
|
||||||
is_intermediate=is_intermediate,
|
is_intermediate=is_intermediate,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -185,14 +192,21 @@ async def get_image_metadata(
|
|||||||
raise HTTPException(status_code=404)
|
raise HTTPException(status_code=404)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowAndGraphResponse(BaseModel):
|
||||||
|
workflow: Optional[str] = Field(description="The workflow used to generate the image, as stringified JSON")
|
||||||
|
graph: Optional[str] = Field(description="The graph used to generate the image, as stringified JSON")
|
||||||
|
|
||||||
|
|
||||||
@images_router.get(
|
@images_router.get(
|
||||||
"/i/{image_name}/workflow", operation_id="get_image_workflow", response_model=Optional[WorkflowWithoutID]
|
"/i/{image_name}/workflow", operation_id="get_image_workflow", response_model=WorkflowAndGraphResponse
|
||||||
)
|
)
|
||||||
async def get_image_workflow(
|
async def get_image_workflow(
|
||||||
image_name: str = Path(description="The name of image whose workflow to get"),
|
image_name: str = Path(description="The name of image whose workflow to get"),
|
||||||
) -> Optional[WorkflowWithoutID]:
|
) -> WorkflowAndGraphResponse:
|
||||||
try:
|
try:
|
||||||
return ApiDependencies.invoker.services.images.get_workflow(image_name)
|
workflow = ApiDependencies.invoker.services.images.get_workflow(image_name)
|
||||||
|
graph = ApiDependencies.invoker.services.images.get_graph(image_name)
|
||||||
|
return WorkflowAndGraphResponse(workflow=workflow, graph=graph)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status_code=404)
|
raise HTTPException(status_code=404)
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import pathlib
|
|||||||
import shutil
|
import shutil
|
||||||
import traceback
|
import traceback
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Type
|
||||||
|
|
||||||
from fastapi import Body, Path, Query, Response, UploadFile
|
from fastapi import Body, Path, Query, Response, UploadFile
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
@ -16,6 +16,7 @@ from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field
|
|||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
from invokeai.app.services.model_images.model_images_common import ModelImageFileNotFoundException
|
||||||
from invokeai.app.services.model_install import ModelInstallJob
|
from invokeai.app.services.model_install import ModelInstallJob
|
||||||
from invokeai.app.services.model_records import (
|
from invokeai.app.services.model_records import (
|
||||||
DuplicateModelException,
|
DuplicateModelException,
|
||||||
@ -52,6 +53,13 @@ class ModelsList(BaseModel):
|
|||||||
model_config = ConfigDict(use_enum_values=True)
|
model_config = ConfigDict(use_enum_values=True)
|
||||||
|
|
||||||
|
|
||||||
|
def add_cover_image_to_model_config(config: AnyModelConfig, dependencies: Type[ApiDependencies]) -> AnyModelConfig:
|
||||||
|
"""Add a cover image URL to a model configuration."""
|
||||||
|
cover_image = dependencies.invoker.services.model_images.get_url(config.key)
|
||||||
|
config.cover_image = cover_image
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# These are example inputs and outputs that are used in places where Swagger
|
# These are example inputs and outputs that are used in places where Swagger
|
||||||
# is unable to generate a correct example.
|
# is unable to generate a correct example.
|
||||||
@ -118,8 +126,7 @@ async def list_model_records(
|
|||||||
record_store.search_by_attr(model_type=model_type, model_name=model_name, model_format=model_format)
|
record_store.search_by_attr(model_type=model_type, model_name=model_name, model_format=model_format)
|
||||||
)
|
)
|
||||||
for model in found_models:
|
for model in found_models:
|
||||||
cover_image = ApiDependencies.invoker.services.model_images.get_url(model.key)
|
model = add_cover_image_to_model_config(model, ApiDependencies)
|
||||||
model.cover_image = cover_image
|
|
||||||
return ModelsList(models=found_models)
|
return ModelsList(models=found_models)
|
||||||
|
|
||||||
|
|
||||||
@ -160,12 +167,9 @@ async def get_model_record(
|
|||||||
key: str = Path(description="Key of the model record to fetch."),
|
key: str = Path(description="Key of the model record to fetch."),
|
||||||
) -> AnyModelConfig:
|
) -> AnyModelConfig:
|
||||||
"""Get a model record"""
|
"""Get a model record"""
|
||||||
record_store = ApiDependencies.invoker.services.model_manager.store
|
|
||||||
try:
|
try:
|
||||||
config: AnyModelConfig = record_store.get_model(key)
|
config = ApiDependencies.invoker.services.model_manager.store.get_model(key)
|
||||||
cover_image = ApiDependencies.invoker.services.model_images.get_url(key)
|
return add_cover_image_to_model_config(config, ApiDependencies)
|
||||||
config.cover_image = cover_image
|
|
||||||
return config
|
|
||||||
except UnknownModelException as e:
|
except UnknownModelException as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
|
||||||
@ -294,14 +298,15 @@ async def update_model_record(
|
|||||||
installer = ApiDependencies.invoker.services.model_manager.install
|
installer = ApiDependencies.invoker.services.model_manager.install
|
||||||
try:
|
try:
|
||||||
record_store.update_model(key, changes=changes)
|
record_store.update_model(key, changes=changes)
|
||||||
model_response: AnyModelConfig = installer.sync_model_path(key)
|
config = installer.sync_model_path(key)
|
||||||
|
config = add_cover_image_to_model_config(config, ApiDependencies)
|
||||||
logger.info(f"Updated model: {key}")
|
logger.info(f"Updated model: {key}")
|
||||||
except UnknownModelException as e:
|
except UnknownModelException as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
raise HTTPException(status_code=409, detail=str(e))
|
raise HTTPException(status_code=409, detail=str(e))
|
||||||
return model_response
|
return config
|
||||||
|
|
||||||
|
|
||||||
@model_manager_router.get(
|
@model_manager_router.get(
|
||||||
@ -648,6 +653,14 @@ async def convert_model(
|
|||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
raise HTTPException(status_code=409, detail=str(e))
|
raise HTTPException(status_code=409, detail=str(e))
|
||||||
|
|
||||||
|
# Update the model image if the model had one
|
||||||
|
try:
|
||||||
|
model_image = ApiDependencies.invoker.services.model_images.get(key)
|
||||||
|
ApiDependencies.invoker.services.model_images.save(model_image, new_key)
|
||||||
|
ApiDependencies.invoker.services.model_images.delete(key)
|
||||||
|
except ModelImageFileNotFoundException:
|
||||||
|
pass
|
||||||
|
|
||||||
# delete the original safetensors file
|
# delete the original safetensors file
|
||||||
installer.delete(key)
|
installer.delete(key)
|
||||||
|
|
||||||
@ -655,7 +668,8 @@ async def convert_model(
|
|||||||
shutil.rmtree(cache_path)
|
shutil.rmtree(cache_path)
|
||||||
|
|
||||||
# return the config record for the new diffusers directory
|
# return the config record for the new diffusers directory
|
||||||
new_config: AnyModelConfig = store.get_model(new_key)
|
new_config = store.get_model(new_key)
|
||||||
|
new_config = add_cover_image_to_model_config(new_config, ApiDependencies)
|
||||||
return new_config
|
return new_config
|
||||||
|
|
||||||
|
|
||||||
|
@ -164,6 +164,12 @@ def custom_openapi() -> dict[str, Any]:
|
|||||||
for schema_key, schema_json in additional_schemas[1]["$defs"].items():
|
for schema_key, schema_json in additional_schemas[1]["$defs"].items():
|
||||||
openapi_schema["components"]["schemas"][schema_key] = schema_json
|
openapi_schema["components"]["schemas"][schema_key] = schema_json
|
||||||
|
|
||||||
|
openapi_schema["components"]["schemas"]["InvocationOutputMap"] = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": [],
|
||||||
|
}
|
||||||
|
|
||||||
# Add a reference to the output type to additionalProperties of the invoker schema
|
# Add a reference to the output type to additionalProperties of the invoker schema
|
||||||
for invoker in all_invocations:
|
for invoker in all_invocations:
|
||||||
invoker_name = invoker.__name__ # type: ignore [attr-defined] # this is a valid attribute
|
invoker_name = invoker.__name__ # type: ignore [attr-defined] # this is a valid attribute
|
||||||
@ -172,6 +178,8 @@ def custom_openapi() -> dict[str, Any]:
|
|||||||
invoker_schema = openapi_schema["components"]["schemas"][f"{invoker_name}"]
|
invoker_schema = openapi_schema["components"]["schemas"][f"{invoker_name}"]
|
||||||
outputs_ref = {"$ref": f"#/components/schemas/{output_type_title}"}
|
outputs_ref = {"$ref": f"#/components/schemas/{output_type_title}"}
|
||||||
invoker_schema["output"] = outputs_ref
|
invoker_schema["output"] = outputs_ref
|
||||||
|
openapi_schema["components"]["schemas"]["InvocationOutputMap"]["properties"][invoker.get_type()] = outputs_ref
|
||||||
|
openapi_schema["components"]["schemas"]["InvocationOutputMap"]["required"].append(invoker.get_type())
|
||||||
invoker_schema["class"] = "invocation"
|
invoker_schema["class"] = "invocation"
|
||||||
|
|
||||||
# This code no longer seems to be necessary?
|
# This code no longer seems to be necessary?
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
|
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
@ -504,7 +503,7 @@ class ImageInverseLerpInvocation(BaseInvocation, WithMetadata, WithBoard):
|
|||||||
title="Blur NSFW Image",
|
title="Blur NSFW Image",
|
||||||
tags=["image", "nsfw"],
|
tags=["image", "nsfw"],
|
||||||
category="image",
|
category="image",
|
||||||
version="1.2.2",
|
version="1.2.3",
|
||||||
)
|
)
|
||||||
class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithBoard):
|
class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||||
"""Add blur to NSFW-flagged images"""
|
"""Add blur to NSFW-flagged images"""
|
||||||
@ -516,23 +515,12 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithBoard):
|
|||||||
|
|
||||||
logger = context.logger
|
logger = context.logger
|
||||||
logger.debug("Running NSFW checker")
|
logger.debug("Running NSFW checker")
|
||||||
if SafetyChecker.has_nsfw_concept(image):
|
image = SafetyChecker.blur_if_nsfw(image)
|
||||||
logger.info("A potentially NSFW image has been detected. Image will be blurred.")
|
|
||||||
blurry_image = image.filter(filter=ImageFilter.GaussianBlur(radius=32))
|
|
||||||
caution = self._get_caution_img()
|
|
||||||
blurry_image.paste(caution, (0, 0), caution)
|
|
||||||
image = blurry_image
|
|
||||||
|
|
||||||
image_dto = context.images.save(image=image)
|
image_dto = context.images.save(image=image)
|
||||||
|
|
||||||
return ImageOutput.build(image_dto)
|
return ImageOutput.build(image_dto)
|
||||||
|
|
||||||
def _get_caution_img(self) -> Image.Image:
|
|
||||||
import invokeai.app.assets.images as image_assets
|
|
||||||
|
|
||||||
caution = Image.open(Path(image_assets.__path__[0]) / "caution.png")
|
|
||||||
return caution.resize((caution.width // 2, caution.height // 2))
|
|
||||||
|
|
||||||
|
|
||||||
@invocation(
|
@invocation(
|
||||||
"img_watermark",
|
"img_watermark",
|
||||||
|
@ -586,13 +586,6 @@ class DenoiseLatentsInvocation(BaseInvocation):
|
|||||||
unet: UNet2DConditionModel,
|
unet: UNet2DConditionModel,
|
||||||
scheduler: Scheduler,
|
scheduler: Scheduler,
|
||||||
) -> StableDiffusionGeneratorPipeline:
|
) -> StableDiffusionGeneratorPipeline:
|
||||||
# TODO:
|
|
||||||
# configure_model_padding(
|
|
||||||
# unet,
|
|
||||||
# self.seamless,
|
|
||||||
# self.seamless_axes,
|
|
||||||
# )
|
|
||||||
|
|
||||||
class FakeVae:
|
class FakeVae:
|
||||||
class FakeVaeConfig:
|
class FakeVaeConfig:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
@ -190,6 +190,75 @@ class LoRALoaderInvocation(BaseInvocation):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
@invocation_output("lora_selector_output")
|
||||||
|
class LoRASelectorOutput(BaseInvocationOutput):
|
||||||
|
"""Model loader output"""
|
||||||
|
|
||||||
|
lora: LoRAField = OutputField(description="LoRA model and weight", title="LoRA")
|
||||||
|
|
||||||
|
|
||||||
|
@invocation("lora_selector", title="LoRA Selector", tags=["model"], category="model", version="1.0.0")
|
||||||
|
class LoRASelectorInvocation(BaseInvocation):
|
||||||
|
"""Selects a LoRA model and weight."""
|
||||||
|
|
||||||
|
lora: ModelIdentifierField = InputField(
|
||||||
|
description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA", ui_type=UIType.LoRAModel
|
||||||
|
)
|
||||||
|
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
|
||||||
|
|
||||||
|
def invoke(self, context: InvocationContext) -> LoRASelectorOutput:
|
||||||
|
return LoRASelectorOutput(lora=LoRAField(lora=self.lora, weight=self.weight))
|
||||||
|
|
||||||
|
|
||||||
|
@invocation("lora_collection_loader", title="LoRA Collection Loader", tags=["model"], category="model", version="1.0.0")
|
||||||
|
class LoRACollectionLoader(BaseInvocation):
|
||||||
|
"""Applies a collection of LoRAs to the provided UNet and CLIP models."""
|
||||||
|
|
||||||
|
loras: LoRAField | list[LoRAField] = InputField(
|
||||||
|
description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
|
||||||
|
)
|
||||||
|
unet: Optional[UNetField] = InputField(
|
||||||
|
default=None,
|
||||||
|
description=FieldDescriptions.unet,
|
||||||
|
input=Input.Connection,
|
||||||
|
title="UNet",
|
||||||
|
)
|
||||||
|
clip: Optional[CLIPField] = InputField(
|
||||||
|
default=None,
|
||||||
|
description=FieldDescriptions.clip,
|
||||||
|
input=Input.Connection,
|
||||||
|
title="CLIP",
|
||||||
|
)
|
||||||
|
|
||||||
|
def invoke(self, context: InvocationContext) -> LoRALoaderOutput:
|
||||||
|
output = LoRALoaderOutput()
|
||||||
|
loras = self.loras if isinstance(self.loras, list) else [self.loras]
|
||||||
|
added_loras: list[str] = []
|
||||||
|
|
||||||
|
for lora in loras:
|
||||||
|
if lora.lora.key in added_loras:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not context.models.exists(lora.lora.key):
|
||||||
|
raise Exception(f"Unknown lora: {lora.lora.key}!")
|
||||||
|
|
||||||
|
assert lora.lora.base in (BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2)
|
||||||
|
|
||||||
|
added_loras.append(lora.lora.key)
|
||||||
|
|
||||||
|
if self.unet is not None:
|
||||||
|
if output.unet is None:
|
||||||
|
output.unet = self.unet.model_copy(deep=True)
|
||||||
|
output.unet.loras.append(lora)
|
||||||
|
|
||||||
|
if self.clip is not None:
|
||||||
|
if output.clip is None:
|
||||||
|
output.clip = self.clip.model_copy(deep=True)
|
||||||
|
output.clip.loras.append(lora)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
@invocation_output("sdxl_lora_loader_output")
|
@invocation_output("sdxl_lora_loader_output")
|
||||||
class SDXLLoRALoaderOutput(BaseInvocationOutput):
|
class SDXLLoRALoaderOutput(BaseInvocationOutput):
|
||||||
"""SDXL LoRA Loader Output"""
|
"""SDXL LoRA Loader Output"""
|
||||||
@ -279,6 +348,72 @@ class SDXLLoRALoaderInvocation(BaseInvocation):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
@invocation(
|
||||||
|
"sdxl_lora_collection_loader",
|
||||||
|
title="SDXL LoRA Collection Loader",
|
||||||
|
tags=["model"],
|
||||||
|
category="model",
|
||||||
|
version="1.0.0",
|
||||||
|
)
|
||||||
|
class SDXLLoRACollectionLoader(BaseInvocation):
|
||||||
|
"""Applies a collection of SDXL LoRAs to the provided UNet and CLIP models."""
|
||||||
|
|
||||||
|
loras: LoRAField | list[LoRAField] = InputField(
|
||||||
|
description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
|
||||||
|
)
|
||||||
|
unet: Optional[UNetField] = InputField(
|
||||||
|
default=None,
|
||||||
|
description=FieldDescriptions.unet,
|
||||||
|
input=Input.Connection,
|
||||||
|
title="UNet",
|
||||||
|
)
|
||||||
|
clip: Optional[CLIPField] = InputField(
|
||||||
|
default=None,
|
||||||
|
description=FieldDescriptions.clip,
|
||||||
|
input=Input.Connection,
|
||||||
|
title="CLIP",
|
||||||
|
)
|
||||||
|
clip2: Optional[CLIPField] = InputField(
|
||||||
|
default=None,
|
||||||
|
description=FieldDescriptions.clip,
|
||||||
|
input=Input.Connection,
|
||||||
|
title="CLIP 2",
|
||||||
|
)
|
||||||
|
|
||||||
|
def invoke(self, context: InvocationContext) -> SDXLLoRALoaderOutput:
|
||||||
|
output = SDXLLoRALoaderOutput()
|
||||||
|
loras = self.loras if isinstance(self.loras, list) else [self.loras]
|
||||||
|
added_loras: list[str] = []
|
||||||
|
|
||||||
|
for lora in loras:
|
||||||
|
if lora.lora.key in added_loras:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not context.models.exists(lora.lora.key):
|
||||||
|
raise Exception(f"Unknown lora: {lora.lora.key}!")
|
||||||
|
|
||||||
|
assert lora.lora.base is BaseModelType.StableDiffusionXL
|
||||||
|
|
||||||
|
added_loras.append(lora.lora.key)
|
||||||
|
|
||||||
|
if self.unet is not None:
|
||||||
|
if output.unet is None:
|
||||||
|
output.unet = self.unet.model_copy(deep=True)
|
||||||
|
output.unet.loras.append(lora)
|
||||||
|
|
||||||
|
if self.clip is not None:
|
||||||
|
if output.clip is None:
|
||||||
|
output.clip = self.clip.model_copy(deep=True)
|
||||||
|
output.clip.loras.append(lora)
|
||||||
|
|
||||||
|
if self.clip2 is not None:
|
||||||
|
if output.clip2 is None:
|
||||||
|
output.clip2 = self.clip2.model_copy(deep=True)
|
||||||
|
output.clip2.loras.append(lora)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.2")
|
@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.2")
|
||||||
class VAELoaderInvocation(BaseInvocation):
|
class VAELoaderInvocation(BaseInvocation):
|
||||||
"""Loads a VAE model, outputting a VaeLoaderOutput"""
|
"""Loads a VAE model, outputting a VaeLoaderOutput"""
|
||||||
|
@ -122,6 +122,8 @@ class EventServiceBase:
|
|||||||
source_node_id: str,
|
source_node_id: str,
|
||||||
error_type: str,
|
error_type: str,
|
||||||
error: str,
|
error: str,
|
||||||
|
user_id: str | None,
|
||||||
|
project_id: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Emitted when an invocation has completed"""
|
"""Emitted when an invocation has completed"""
|
||||||
self.__emit_queue_event(
|
self.__emit_queue_event(
|
||||||
@ -135,6 +137,8 @@ class EventServiceBase:
|
|||||||
"source_node_id": source_node_id,
|
"source_node_id": source_node_id,
|
||||||
"error_type": error_type,
|
"error_type": error_type,
|
||||||
"error": error,
|
"error": error,
|
||||||
|
"user_id": user_id,
|
||||||
|
"project_id": project_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,9 +4,6 @@ from typing import Optional
|
|||||||
|
|
||||||
from PIL.Image import Image as PILImageType
|
from PIL.Image import Image as PILImageType
|
||||||
|
|
||||||
from invokeai.app.invocations.fields import MetadataField
|
|
||||||
from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID
|
|
||||||
|
|
||||||
|
|
||||||
class ImageFileStorageBase(ABC):
|
class ImageFileStorageBase(ABC):
|
||||||
"""Low-level service responsible for storing and retrieving image files."""
|
"""Low-level service responsible for storing and retrieving image files."""
|
||||||
@ -33,8 +30,9 @@ class ImageFileStorageBase(ABC):
|
|||||||
self,
|
self,
|
||||||
image: PILImageType,
|
image: PILImageType,
|
||||||
image_name: str,
|
image_name: str,
|
||||||
metadata: Optional[MetadataField] = None,
|
metadata: Optional[str] = None,
|
||||||
workflow: Optional[WorkflowWithoutID] = None,
|
workflow: Optional[str] = None,
|
||||||
|
graph: 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."""
|
||||||
@ -46,6 +44,11 @@ class ImageFileStorageBase(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]:
|
def get_workflow(self, image_name: str) -> Optional[str]:
|
||||||
"""Gets the workflow of an image."""
|
"""Gets the workflow of an image."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_graph(self, image_name: str) -> Optional[str]:
|
||||||
|
"""Gets the graph of an image."""
|
||||||
|
pass
|
||||||
|
@ -7,9 +7,7 @@ from PIL import Image, PngImagePlugin
|
|||||||
from PIL.Image import Image as PILImageType
|
from PIL.Image import Image as PILImageType
|
||||||
from send2trash import send2trash
|
from send2trash import send2trash
|
||||||
|
|
||||||
from invokeai.app.invocations.fields import MetadataField
|
|
||||||
from invokeai.app.services.invoker import Invoker
|
from invokeai.app.services.invoker import Invoker
|
||||||
from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID
|
|
||||||
from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail
|
from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail
|
||||||
|
|
||||||
from .image_files_base import ImageFileStorageBase
|
from .image_files_base import ImageFileStorageBase
|
||||||
@ -56,8 +54,9 @@ class DiskImageFileStorage(ImageFileStorageBase):
|
|||||||
self,
|
self,
|
||||||
image: PILImageType,
|
image: PILImageType,
|
||||||
image_name: str,
|
image_name: str,
|
||||||
metadata: Optional[MetadataField] = None,
|
metadata: Optional[str] = None,
|
||||||
workflow: Optional[WorkflowWithoutID] = None,
|
workflow: Optional[str] = None,
|
||||||
|
graph: Optional[str] = None,
|
||||||
thumbnail_size: int = 256,
|
thumbnail_size: int = 256,
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
@ -68,13 +67,14 @@ class DiskImageFileStorage(ImageFileStorageBase):
|
|||||||
info_dict = {}
|
info_dict = {}
|
||||||
|
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
metadata_json = metadata.model_dump_json()
|
info_dict["invokeai_metadata"] = metadata
|
||||||
info_dict["invokeai_metadata"] = metadata_json
|
pnginfo.add_text("invokeai_metadata", metadata)
|
||||||
pnginfo.add_text("invokeai_metadata", metadata_json)
|
|
||||||
if workflow is not None:
|
if workflow is not None:
|
||||||
workflow_json = workflow.model_dump_json()
|
info_dict["invokeai_workflow"] = workflow
|
||||||
info_dict["invokeai_workflow"] = workflow_json
|
pnginfo.add_text("invokeai_workflow", workflow)
|
||||||
pnginfo.add_text("invokeai_workflow", workflow_json)
|
if graph is not None:
|
||||||
|
info_dict["invokeai_graph"] = graph
|
||||||
|
pnginfo.add_text("invokeai_graph", graph)
|
||||||
|
|
||||||
# When saving the image, the image object's info field is not populated. We need to set it
|
# When saving the image, the image object's info field is not populated. We need to set it
|
||||||
image.info = info_dict
|
image.info = info_dict
|
||||||
@ -129,11 +129,18 @@ class DiskImageFileStorage(ImageFileStorageBase):
|
|||||||
path = path if isinstance(path, Path) else Path(path)
|
path = path if isinstance(path, Path) else Path(path)
|
||||||
return path.exists()
|
return path.exists()
|
||||||
|
|
||||||
def get_workflow(self, image_name: str) -> WorkflowWithoutID | None:
|
def get_workflow(self, image_name: str) -> str | None:
|
||||||
image = self.get(image_name)
|
image = self.get(image_name)
|
||||||
workflow = image.info.get("invokeai_workflow", None)
|
workflow = image.info.get("invokeai_workflow", None)
|
||||||
if workflow is not None:
|
if isinstance(workflow, str):
|
||||||
return WorkflowWithoutID.model_validate_json(workflow)
|
return workflow
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_graph(self, image_name: str) -> str | None:
|
||||||
|
image = self.get(image_name)
|
||||||
|
graph = image.info.get("invokeai_graph", None)
|
||||||
|
if isinstance(graph, str):
|
||||||
|
return graph
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __validate_storage_folders(self) -> None:
|
def __validate_storage_folders(self) -> None:
|
||||||
|
@ -80,7 +80,7 @@ class ImageRecordStorageBase(ABC):
|
|||||||
starred: Optional[bool] = False,
|
starred: Optional[bool] = False,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
metadata: Optional[MetadataField] = None,
|
metadata: Optional[str] = None,
|
||||||
) -> datetime:
|
) -> datetime:
|
||||||
"""Saves an image record."""
|
"""Saves an image record."""
|
||||||
pass
|
pass
|
||||||
|
@ -328,10 +328,9 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
|||||||
starred: Optional[bool] = False,
|
starred: Optional[bool] = False,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
metadata: Optional[MetadataField] = None,
|
metadata: Optional[str] = None,
|
||||||
) -> datetime:
|
) -> datetime:
|
||||||
try:
|
try:
|
||||||
metadata_json = metadata.model_dump_json() if metadata is not None else None
|
|
||||||
self._lock.acquire()
|
self._lock.acquire()
|
||||||
self._cursor.execute(
|
self._cursor.execute(
|
||||||
"""--sql
|
"""--sql
|
||||||
@ -358,7 +357,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
|||||||
height,
|
height,
|
||||||
node_id,
|
node_id,
|
||||||
session_id,
|
session_id,
|
||||||
metadata_json,
|
metadata,
|
||||||
is_intermediate,
|
is_intermediate,
|
||||||
starred,
|
starred,
|
||||||
has_workflow,
|
has_workflow,
|
||||||
|
@ -12,7 +12,6 @@ from invokeai.app.services.image_records.image_records_common import (
|
|||||||
)
|
)
|
||||||
from invokeai.app.services.images.images_common import ImageDTO
|
from invokeai.app.services.images.images_common import ImageDTO
|
||||||
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
|
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
|
||||||
from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID
|
|
||||||
|
|
||||||
|
|
||||||
class ImageServiceABC(ABC):
|
class ImageServiceABC(ABC):
|
||||||
@ -51,8 +50,9 @@ class ImageServiceABC(ABC):
|
|||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
board_id: Optional[str] = None,
|
board_id: Optional[str] = None,
|
||||||
is_intermediate: Optional[bool] = False,
|
is_intermediate: Optional[bool] = False,
|
||||||
metadata: Optional[MetadataField] = None,
|
metadata: Optional[str] = None,
|
||||||
workflow: Optional[WorkflowWithoutID] = None,
|
workflow: Optional[str] = None,
|
||||||
|
graph: 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
|
||||||
@ -87,7 +87,12 @@ class ImageServiceABC(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]:
|
def get_workflow(self, image_name: str) -> Optional[str]:
|
||||||
|
"""Gets an image's workflow."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_graph(self, image_name: str) -> Optional[str]:
|
||||||
"""Gets an image's workflow."""
|
"""Gets an image's workflow."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ from PIL.Image import Image as PILImageType
|
|||||||
from invokeai.app.invocations.fields import MetadataField
|
from invokeai.app.invocations.fields import MetadataField
|
||||||
from invokeai.app.services.invoker import Invoker
|
from invokeai.app.services.invoker import Invoker
|
||||||
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
|
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
|
||||||
from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID
|
|
||||||
|
|
||||||
from ..image_files.image_files_common import (
|
from ..image_files.image_files_common import (
|
||||||
ImageFileDeleteException,
|
ImageFileDeleteException,
|
||||||
@ -42,8 +41,9 @@ class ImageService(ImageServiceABC):
|
|||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
board_id: Optional[str] = None,
|
board_id: Optional[str] = None,
|
||||||
is_intermediate: Optional[bool] = False,
|
is_intermediate: Optional[bool] = False,
|
||||||
metadata: Optional[MetadataField] = None,
|
metadata: Optional[str] = None,
|
||||||
workflow: Optional[WorkflowWithoutID] = None,
|
workflow: Optional[str] = None,
|
||||||
|
graph: Optional[str] = None,
|
||||||
) -> ImageDTO:
|
) -> ImageDTO:
|
||||||
if image_origin not in ResourceOrigin:
|
if image_origin not in ResourceOrigin:
|
||||||
raise InvalidOriginException
|
raise InvalidOriginException
|
||||||
@ -64,7 +64,7 @@ class ImageService(ImageServiceABC):
|
|||||||
image_category=image_category,
|
image_category=image_category,
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
has_workflow=workflow is not None,
|
has_workflow=workflow is not None or graph is not None,
|
||||||
# Meta fields
|
# Meta fields
|
||||||
is_intermediate=is_intermediate,
|
is_intermediate=is_intermediate,
|
||||||
# Nullable fields
|
# Nullable fields
|
||||||
@ -75,7 +75,7 @@ class ImageService(ImageServiceABC):
|
|||||||
if board_id is not None:
|
if board_id is not None:
|
||||||
self.__invoker.services.board_image_records.add_image_to_board(board_id=board_id, image_name=image_name)
|
self.__invoker.services.board_image_records.add_image_to_board(board_id=board_id, image_name=image_name)
|
||||||
self.__invoker.services.image_files.save(
|
self.__invoker.services.image_files.save(
|
||||||
image_name=image_name, image=image, metadata=metadata, workflow=workflow
|
image_name=image_name, image=image, metadata=metadata, workflow=workflow, graph=graph
|
||||||
)
|
)
|
||||||
image_dto = self.get_dto(image_name)
|
image_dto = self.get_dto(image_name)
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ class ImageService(ImageServiceABC):
|
|||||||
self.__invoker.services.logger.error("Problem getting image metadata")
|
self.__invoker.services.logger.error("Problem getting image metadata")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]:
|
def get_workflow(self, image_name: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
return self.__invoker.services.image_files.get_workflow(image_name)
|
return self.__invoker.services.image_files.get_workflow(image_name)
|
||||||
except ImageFileNotFoundException:
|
except ImageFileNotFoundException:
|
||||||
@ -167,6 +167,16 @@ class ImageService(ImageServiceABC):
|
|||||||
self.__invoker.services.logger.error("Problem getting image workflow")
|
self.__invoker.services.logger.error("Problem getting image workflow")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def get_graph(self, image_name: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
return self.__invoker.services.image_files.get_graph(image_name)
|
||||||
|
except ImageFileNotFoundException:
|
||||||
|
self.__invoker.services.logger.error("Image file not found")
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
self.__invoker.services.logger.error("Problem getting image graph")
|
||||||
|
raise
|
||||||
|
|
||||||
def get_path(self, image_name: str, thumbnail: bool = False) -> str:
|
def get_path(self, image_name: str, thumbnail: bool = False) -> str:
|
||||||
try:
|
try:
|
||||||
return str(self.__invoker.services.image_files.get_path(image_name, thumbnail))
|
return str(self.__invoker.services.image_files.get_path(image_name, thumbnail))
|
||||||
|
@ -237,6 +237,8 @@ class DefaultSessionProcessor(SessionProcessorBase):
|
|||||||
source_node_id=source_invocation_id,
|
source_node_id=source_invocation_id,
|
||||||
error_type=e.__class__.__name__,
|
error_type=e.__class__.__name__,
|
||||||
error=error,
|
error=error,
|
||||||
|
user_id=None,
|
||||||
|
project_id=None,
|
||||||
)
|
)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -181,9 +181,9 @@ class ImagesInterface(InvocationContextInterface):
|
|||||||
# If `metadata` is provided directly, use that. Else, use the metadata provided by `WithMetadata`, falling back to None.
|
# If `metadata` is provided directly, use that. Else, use the metadata provided by `WithMetadata`, falling back to None.
|
||||||
metadata_ = None
|
metadata_ = None
|
||||||
if metadata:
|
if metadata:
|
||||||
metadata_ = metadata
|
metadata_ = metadata.model_dump_json()
|
||||||
elif isinstance(self._data.invocation, WithMetadata):
|
elif isinstance(self._data.invocation, WithMetadata) and self._data.invocation.metadata:
|
||||||
metadata_ = self._data.invocation.metadata
|
metadata_ = self._data.invocation.metadata.model_dump_json()
|
||||||
|
|
||||||
# If `board_id` is provided directly, use that. Else, use the board provided by `WithBoard`, falling back to None.
|
# If `board_id` is provided directly, use that. Else, use the board provided by `WithBoard`, falling back to None.
|
||||||
board_id_ = None
|
board_id_ = None
|
||||||
@ -192,6 +192,14 @@ class ImagesInterface(InvocationContextInterface):
|
|||||||
elif isinstance(self._data.invocation, WithBoard) and self._data.invocation.board:
|
elif isinstance(self._data.invocation, WithBoard) and self._data.invocation.board:
|
||||||
board_id_ = self._data.invocation.board.board_id
|
board_id_ = self._data.invocation.board.board_id
|
||||||
|
|
||||||
|
workflow_ = None
|
||||||
|
if self._data.queue_item.workflow:
|
||||||
|
workflow_ = self._data.queue_item.workflow.model_dump_json()
|
||||||
|
|
||||||
|
graph_ = None
|
||||||
|
if self._data.queue_item.session.graph:
|
||||||
|
graph_ = self._data.queue_item.session.graph.model_dump_json()
|
||||||
|
|
||||||
return self._services.images.create(
|
return self._services.images.create(
|
||||||
image=image,
|
image=image,
|
||||||
is_intermediate=self._data.invocation.is_intermediate,
|
is_intermediate=self._data.invocation.is_intermediate,
|
||||||
@ -199,7 +207,8 @@ class ImagesInterface(InvocationContextInterface):
|
|||||||
board_id=board_id_,
|
board_id=board_id_,
|
||||||
metadata=metadata_,
|
metadata=metadata_,
|
||||||
image_origin=ResourceOrigin.INTERNAL,
|
image_origin=ResourceOrigin.INTERNAL,
|
||||||
workflow=self._data.queue_item.workflow,
|
workflow=workflow_,
|
||||||
|
graph=graph_,
|
||||||
session_id=self._data.queue_item.session_id,
|
session_id=self._data.queue_item.session_id,
|
||||||
node_id=self._data.invocation.id,
|
node_id=self._data.invocation.id,
|
||||||
)
|
)
|
||||||
|
@ -4,5 +4,4 @@ Initialization file for invokeai.backend.image_util methods.
|
|||||||
|
|
||||||
from .infill_methods.patchmatch import PatchMatch # noqa: F401
|
from .infill_methods.patchmatch import PatchMatch # noqa: F401
|
||||||
from .pngwriter import PngWriter, PromptFormatter, retrieve_metadata, write_metadata # noqa: F401
|
from .pngwriter import PngWriter, PromptFormatter, retrieve_metadata, write_metadata # noqa: F401
|
||||||
from .seamless import configure_model_padding # noqa: F401
|
|
||||||
from .util import InitImageResizer, make_grid # noqa: F401
|
from .util import InitImageResizer, make_grid # noqa: F401
|
||||||
|
@ -8,7 +8,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker
|
from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker
|
||||||
from PIL import Image
|
from PIL import Image, ImageFilter
|
||||||
from transformers import AutoFeatureExtractor
|
from transformers import AutoFeatureExtractor
|
||||||
|
|
||||||
import invokeai.backend.util.logging as logger
|
import invokeai.backend.util.logging as logger
|
||||||
@ -16,6 +16,7 @@ from invokeai.app.services.config.config_default import get_config
|
|||||||
from invokeai.backend.util.devices import TorchDevice
|
from invokeai.backend.util.devices import TorchDevice
|
||||||
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
||||||
|
|
||||||
|
repo_id = "CompVis/stable-diffusion-safety-checker"
|
||||||
CHECKER_PATH = "core/convert/stable-diffusion-safety-checker"
|
CHECKER_PATH = "core/convert/stable-diffusion-safety-checker"
|
||||||
|
|
||||||
|
|
||||||
@ -24,30 +25,30 @@ class SafetyChecker:
|
|||||||
Wrapper around SafetyChecker model.
|
Wrapper around SafetyChecker model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
safety_checker = None
|
|
||||||
feature_extractor = None
|
feature_extractor = None
|
||||||
tried_load: bool = False
|
safety_checker = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _load_safety_checker(cls):
|
def _load_safety_checker(cls):
|
||||||
if cls.tried_load:
|
if cls.safety_checker is not None and cls.feature_extractor is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(get_config().models_path / CHECKER_PATH)
|
model_path = get_config().models_path / CHECKER_PATH
|
||||||
cls.feature_extractor = AutoFeatureExtractor.from_pretrained(get_config().models_path / CHECKER_PATH)
|
if model_path.exists():
|
||||||
|
cls.feature_extractor = AutoFeatureExtractor.from_pretrained(model_path)
|
||||||
|
cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(model_path)
|
||||||
|
else:
|
||||||
|
model_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
cls.feature_extractor = AutoFeatureExtractor.from_pretrained(repo_id)
|
||||||
|
cls.feature_extractor.save_pretrained(model_path, safe_serialization=True)
|
||||||
|
cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(repo_id)
|
||||||
|
cls.safety_checker.save_pretrained(model_path, safe_serialization=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Could not load NSFW checker: {str(e)}")
|
logger.warning(f"Could not load NSFW checker: {str(e)}")
|
||||||
cls.tried_load = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def safety_checker_available(cls) -> bool:
|
|
||||||
return Path(get_config().models_path, CHECKER_PATH).exists()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def has_nsfw_concept(cls, image: Image.Image) -> bool:
|
def has_nsfw_concept(cls, image: Image.Image) -> bool:
|
||||||
if not cls.safety_checker_available() and cls.tried_load:
|
|
||||||
return False
|
|
||||||
cls._load_safety_checker()
|
cls._load_safety_checker()
|
||||||
if cls.safety_checker is None or cls.feature_extractor is None:
|
if cls.safety_checker is None or cls.feature_extractor is None:
|
||||||
return False
|
return False
|
||||||
@ -60,3 +61,24 @@ class SafetyChecker:
|
|||||||
with SilenceWarnings():
|
with SilenceWarnings():
|
||||||
checked_image, has_nsfw_concept = cls.safety_checker(images=x_image, clip_input=features.pixel_values)
|
checked_image, has_nsfw_concept = cls.safety_checker(images=x_image, clip_input=features.pixel_values)
|
||||||
return has_nsfw_concept[0]
|
return has_nsfw_concept[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def blur_if_nsfw(cls, image: Image.Image) -> Image.Image:
|
||||||
|
if cls.has_nsfw_concept(image):
|
||||||
|
logger.warning("A potentially NSFW image has been detected. Image will be blurred.")
|
||||||
|
blurry_image = image.filter(filter=ImageFilter.GaussianBlur(radius=32))
|
||||||
|
caution = cls._get_caution_img()
|
||||||
|
# Center the caution image on the blurred image
|
||||||
|
x = (blurry_image.width - caution.width) // 2
|
||||||
|
y = (blurry_image.height - caution.height) // 2
|
||||||
|
blurry_image.paste(caution, (x, y), caution)
|
||||||
|
image = blurry_image
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_caution_img(cls) -> Image.Image:
|
||||||
|
import invokeai.app.assets.images as image_assets
|
||||||
|
|
||||||
|
caution = Image.open(Path(image_assets.__path__[0]) / "caution.png")
|
||||||
|
return caution.resize((caution.width // 2, caution.height // 2))
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
import torch.nn as nn
|
|
||||||
|
|
||||||
|
|
||||||
def _conv_forward_asymmetric(self, input, weight, bias):
|
|
||||||
"""
|
|
||||||
Patch for Conv2d._conv_forward that supports asymmetric padding
|
|
||||||
"""
|
|
||||||
working = nn.functional.pad(input, self.asymmetric_padding["x"], mode=self.asymmetric_padding_mode["x"])
|
|
||||||
working = nn.functional.pad(working, self.asymmetric_padding["y"], mode=self.asymmetric_padding_mode["y"])
|
|
||||||
return nn.functional.conv2d(
|
|
||||||
working,
|
|
||||||
weight,
|
|
||||||
bias,
|
|
||||||
self.stride,
|
|
||||||
nn.modules.utils._pair(0),
|
|
||||||
self.dilation,
|
|
||||||
self.groups,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def configure_model_padding(model, seamless, seamless_axes):
|
|
||||||
"""
|
|
||||||
Modifies the 2D convolution layers to use a circular padding mode based on
|
|
||||||
the `seamless` and `seamless_axes` options.
|
|
||||||
"""
|
|
||||||
# TODO: get an explicit interface for this in diffusers: https://github.com/huggingface/diffusers/issues/556
|
|
||||||
for m in model.modules():
|
|
||||||
if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
|
|
||||||
if seamless:
|
|
||||||
m.asymmetric_padding_mode = {}
|
|
||||||
m.asymmetric_padding = {}
|
|
||||||
m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant"
|
|
||||||
m.asymmetric_padding["x"] = (
|
|
||||||
m._reversed_padding_repeated_twice[0],
|
|
||||||
m._reversed_padding_repeated_twice[1],
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant"
|
|
||||||
m.asymmetric_padding["y"] = (
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
m._reversed_padding_repeated_twice[2],
|
|
||||||
m._reversed_padding_repeated_twice[3],
|
|
||||||
)
|
|
||||||
m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d)
|
|
||||||
else:
|
|
||||||
m._conv_forward = nn.Conv2d._conv_forward.__get__(m, nn.Conv2d)
|
|
||||||
if hasattr(m, "asymmetric_padding_mode"):
|
|
||||||
del m.asymmetric_padding_mode
|
|
||||||
if hasattr(m, "asymmetric_padding"):
|
|
||||||
del m.asymmetric_padding
|
|
@ -1,89 +1,51 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Callable, List, Union
|
from typing import Callable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import torch
|
||||||
import torch.nn as nn
|
import torch.nn as nn
|
||||||
from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
|
from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
|
||||||
from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny
|
from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny
|
||||||
|
from diffusers.models.lora import LoRACompatibleConv
|
||||||
from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel
|
from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel
|
||||||
|
|
||||||
|
|
||||||
def _conv_forward_asymmetric(self, input, weight, bias):
|
|
||||||
"""
|
|
||||||
Patch for Conv2d._conv_forward that supports asymmetric padding
|
|
||||||
"""
|
|
||||||
working = nn.functional.pad(input, self.asymmetric_padding["x"], mode=self.asymmetric_padding_mode["x"])
|
|
||||||
working = nn.functional.pad(working, self.asymmetric_padding["y"], mode=self.asymmetric_padding_mode["y"])
|
|
||||||
return nn.functional.conv2d(
|
|
||||||
working,
|
|
||||||
weight,
|
|
||||||
bias,
|
|
||||||
self.stride,
|
|
||||||
nn.modules.utils._pair(0),
|
|
||||||
self.dilation,
|
|
||||||
self.groups,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL, AutoencoderTiny], seamless_axes: List[str]):
|
def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL, AutoencoderTiny], seamless_axes: List[str]):
|
||||||
if not seamless_axes:
|
if not seamless_axes:
|
||||||
yield
|
yield
|
||||||
return
|
return
|
||||||
|
|
||||||
# Callable: (input: Tensor, weight: Tensor, bias: Optional[Tensor]) -> Tensor
|
# override conv_forward
|
||||||
to_restore: list[tuple[nn.Conv2d | nn.ConvTranspose2d, Callable]] = []
|
# https://github.com/huggingface/diffusers/issues/556#issuecomment-1993287019
|
||||||
|
def _conv_forward_asymmetric(self, input: torch.Tensor, weight: torch.Tensor, bias: Optional[torch.Tensor] = None):
|
||||||
|
self.paddingX = (self._reversed_padding_repeated_twice[0], self._reversed_padding_repeated_twice[1], 0, 0)
|
||||||
|
self.paddingY = (0, 0, self._reversed_padding_repeated_twice[2], self._reversed_padding_repeated_twice[3])
|
||||||
|
working = torch.nn.functional.pad(input, self.paddingX, mode=x_mode)
|
||||||
|
working = torch.nn.functional.pad(working, self.paddingY, mode=y_mode)
|
||||||
|
return torch.nn.functional.conv2d(
|
||||||
|
working, weight, bias, self.stride, torch.nn.modules.utils._pair(0), self.dilation, self.groups
|
||||||
|
)
|
||||||
|
|
||||||
|
original_layers: List[Tuple[nn.Conv2d, Callable]] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Hard coded to skip down block layers, allowing for seamless tiling at the expense of prompt adherence
|
x_mode = "circular" if "x" in seamless_axes else "constant"
|
||||||
skipped_layers = 1
|
y_mode = "circular" if "y" in seamless_axes else "constant"
|
||||||
for m_name, m in model.named_modules():
|
|
||||||
if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(model, UNet2DConditionModel) and m_name.startswith("down_blocks.") and ".resnets." in m_name:
|
conv_layers: List[torch.nn.Conv2d] = []
|
||||||
# down_blocks.1.resnets.1.conv1
|
|
||||||
_, block_num, _, resnet_num, submodule_name = m_name.split(".")
|
|
||||||
block_num = int(block_num)
|
|
||||||
resnet_num = int(resnet_num)
|
|
||||||
|
|
||||||
if block_num >= len(model.down_blocks) - skipped_layers:
|
for module in model.modules():
|
||||||
continue
|
if isinstance(module, torch.nn.Conv2d):
|
||||||
|
conv_layers.append(module)
|
||||||
|
|
||||||
# Skip the second resnet (could be configurable)
|
for layer in conv_layers:
|
||||||
if resnet_num > 0:
|
if isinstance(layer, LoRACompatibleConv) and layer.lora_layer is None:
|
||||||
continue
|
layer.lora_layer = lambda *x: 0
|
||||||
|
original_layers.append((layer, layer._conv_forward))
|
||||||
# Skip Conv2d layers (could be configurable)
|
layer._conv_forward = _conv_forward_asymmetric.__get__(layer, torch.nn.Conv2d)
|
||||||
if submodule_name == "conv2":
|
|
||||||
continue
|
|
||||||
|
|
||||||
m.asymmetric_padding_mode = {}
|
|
||||||
m.asymmetric_padding = {}
|
|
||||||
m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant"
|
|
||||||
m.asymmetric_padding["x"] = (
|
|
||||||
m._reversed_padding_repeated_twice[0],
|
|
||||||
m._reversed_padding_repeated_twice[1],
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant"
|
|
||||||
m.asymmetric_padding["y"] = (
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
m._reversed_padding_repeated_twice[2],
|
|
||||||
m._reversed_padding_repeated_twice[3],
|
|
||||||
)
|
|
||||||
|
|
||||||
to_restore.append((m, m._conv_forward))
|
|
||||||
m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d)
|
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
for module, orig_conv_forward in to_restore:
|
for layer, orig_conv_forward in original_layers:
|
||||||
module._conv_forward = orig_conv_forward
|
layer._conv_forward = orig_conv_forward
|
||||||
if hasattr(module, "asymmetric_padding_mode"):
|
|
||||||
del module.asymmetric_padding_mode
|
|
||||||
if hasattr(module, "asymmetric_padding"):
|
|
||||||
del module.asymmetric_padding
|
|
||||||
|
@ -10,6 +10,8 @@ module.exports = {
|
|||||||
'path/no-relative-imports': ['error', { maxDepth: 0 }],
|
'path/no-relative-imports': ['error', { maxDepth: 0 }],
|
||||||
// https://github.com/edvardchen/eslint-plugin-i18next/blob/HEAD/docs/rules/no-literal-string.md
|
// https://github.com/edvardchen/eslint-plugin-i18next/blob/HEAD/docs/rules/no-literal-string.md
|
||||||
'i18next/no-literal-string': 'error',
|
'i18next/no-literal-string': 'error',
|
||||||
|
// https://eslint.org/docs/latest/rules/no-console
|
||||||
|
'no-console': 'error',
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
/**
|
/**
|
||||||
|
3
invokeai/frontend/web/.gitignore
vendored
3
invokeai/frontend/web/.gitignore
vendored
@ -43,4 +43,5 @@ stats.html
|
|||||||
yalc.lock
|
yalc.lock
|
||||||
|
|
||||||
# vitest
|
# vitest
|
||||||
tsconfig.vitest-temp.json
|
tsconfig.vitest-temp.json
|
||||||
|
coverage/
|
@ -35,6 +35,7 @@
|
|||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --coverage --ui",
|
||||||
"test:no-watch": "vitest --no-watch"
|
"test:no-watch": "vitest --no-watch"
|
||||||
},
|
},
|
||||||
"madge": {
|
"madge": {
|
||||||
@ -52,47 +53,48 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react-use-size": "^2.1.0",
|
"@chakra-ui/react-use-size": "^2.1.0",
|
||||||
"@dagrejs/dagre": "^1.1.1",
|
"@dagrejs/dagre": "^1.1.2",
|
||||||
"@dagrejs/graphlib": "^2.2.1",
|
"@dagrejs/graphlib": "^2.2.2",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@fontsource-variable/inter": "^5.0.17",
|
"@fontsource-variable/inter": "^5.0.18",
|
||||||
"@invoke-ai/ui-library": "^0.0.25",
|
"@invoke-ai/ui-library": "^0.0.25",
|
||||||
"@nanostores/react": "^0.7.2",
|
"@nanostores/react": "^0.7.2",
|
||||||
"@reduxjs/toolkit": "2.2.2",
|
"@reduxjs/toolkit": "2.2.3",
|
||||||
"@roarr/browser-log-writer": "^1.3.0",
|
"@roarr/browser-log-writer": "^1.3.0",
|
||||||
"chakra-react-select": "^4.7.6",
|
"chakra-react-select": "^4.7.6",
|
||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
"dateformat": "^5.0.3",
|
"dateformat": "^5.0.3",
|
||||||
"framer-motion": "^11.0.22",
|
"fracturedjsonjs": "^4.0.1",
|
||||||
"i18next": "^23.10.1",
|
"framer-motion": "^11.1.8",
|
||||||
"i18next-http-backend": "^2.5.0",
|
"i18next": "^23.11.3",
|
||||||
|
"i18next-http-backend": "^2.5.1",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"jsondiffpatch": "^0.6.0",
|
"jsondiffpatch": "^0.6.0",
|
||||||
"konva": "^9.3.6",
|
"konva": "^9.3.6",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nanostores": "^0.10.0",
|
"nanostores": "^0.10.3",
|
||||||
"new-github-issue-url": "^1.0.0",
|
"new-github-issue-url": "^1.0.0",
|
||||||
"overlayscrollbars": "^2.6.1",
|
"overlayscrollbars": "^2.7.3",
|
||||||
"overlayscrollbars-react": "^0.5.5",
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"query-string": "^9.0.0",
|
"query-string": "^9.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.3.1",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-error-boundary": "^4.0.13",
|
"react-error-boundary": "^4.0.13",
|
||||||
"react-hook-form": "^7.51.2",
|
"react-hook-form": "^7.51.4",
|
||||||
"react-hotkeys-hook": "4.5.0",
|
"react-hotkeys-hook": "4.5.0",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.1",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^5.2.0",
|
||||||
"react-konva": "^18.2.10",
|
"react-konva": "^18.2.10",
|
||||||
"react-redux": "9.1.0",
|
"react-redux": "9.1.2",
|
||||||
"react-resizable-panels": "^2.0.16",
|
"react-resizable-panels": "^2.0.19",
|
||||||
"react-select": "5.8.0",
|
"react-select": "5.8.0",
|
||||||
"react-use": "^17.5.0",
|
"react-use": "^17.5.0",
|
||||||
"react-virtuoso": "^4.7.5",
|
"react-virtuoso": "^4.7.10",
|
||||||
"reactflow": "^11.10.4",
|
"reactflow": "^11.11.3",
|
||||||
"redux-dynamic-middlewares": "^2.2.0",
|
"redux-dynamic-middlewares": "^2.2.0",
|
||||||
"redux-remember": "^5.1.0",
|
"redux-remember": "^5.1.0",
|
||||||
"redux-undo": "^1.1.0",
|
"redux-undo": "^1.1.0",
|
||||||
@ -104,8 +106,8 @@
|
|||||||
"use-device-pixel-ratio": "^1.1.2",
|
"use-device-pixel-ratio": "^1.1.2",
|
||||||
"use-image": "^1.1.1",
|
"use-image": "^1.1.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.23.6",
|
||||||
"zod-validation-error": "^3.0.3"
|
"zod-validation-error": "^3.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@chakra-ui/react": "^2.8.2",
|
"@chakra-ui/react": "^2.8.2",
|
||||||
@ -116,40 +118,42 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@invoke-ai/eslint-config-react": "^0.0.14",
|
"@invoke-ai/eslint-config-react": "^0.0.14",
|
||||||
"@invoke-ai/prettier-config-react": "^0.0.7",
|
"@invoke-ai/prettier-config-react": "^0.0.7",
|
||||||
"@storybook/addon-essentials": "^8.0.4",
|
"@storybook/addon-essentials": "^8.0.10",
|
||||||
"@storybook/addon-interactions": "^8.0.4",
|
"@storybook/addon-interactions": "^8.0.10",
|
||||||
"@storybook/addon-links": "^8.0.4",
|
"@storybook/addon-links": "^8.0.10",
|
||||||
"@storybook/addon-storysource": "^8.0.4",
|
"@storybook/addon-storysource": "^8.0.10",
|
||||||
"@storybook/manager-api": "^8.0.4",
|
"@storybook/manager-api": "^8.0.10",
|
||||||
"@storybook/react": "^8.0.4",
|
"@storybook/react": "^8.0.10",
|
||||||
"@storybook/react-vite": "^8.0.4",
|
"@storybook/react-vite": "^8.0.10",
|
||||||
"@storybook/theming": "^8.0.4",
|
"@storybook/theming": "^8.0.10",
|
||||||
"@types/dateformat": "^5.0.2",
|
"@types/dateformat": "^5.0.2",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.12.10",
|
||||||
"@types/react": "^18.2.73",
|
"@types/react": "^18.3.1",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||||
|
"@vitest/coverage-v8": "^1.5.0",
|
||||||
|
"@vitest/ui": "^1.5.0",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"dpdm": "^3.14.0",
|
"dpdm": "^3.14.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-i18next": "^6.0.3",
|
"eslint-plugin-i18next": "^6.0.3",
|
||||||
"eslint-plugin-path": "^1.3.0",
|
"eslint-plugin-path": "^1.3.0",
|
||||||
"knip": "^5.6.1",
|
"knip": "^5.12.3",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"openapi-typescript": "^6.7.5",
|
"openapi-typescript": "^6.7.5",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"storybook": "^8.0.4",
|
"storybook": "^8.0.10",
|
||||||
"ts-toolbelt": "^9.6.0",
|
"ts-toolbelt": "^9.6.0",
|
||||||
"tsafe": "^1.6.6",
|
"tsafe": "^1.6.6",
|
||||||
"typescript": "^5.4.3",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.6",
|
"vite": "^5.2.11",
|
||||||
"vite-plugin-css-injected-by-js": "^3.5.0",
|
"vite-plugin-css-injected-by-js": "^3.5.1",
|
||||||
"vite-plugin-dts": "^3.8.0",
|
"vite-plugin-dts": "^3.9.1",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
"vitest": "^1.4.0"
|
"vitest": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -76,7 +76,9 @@
|
|||||||
"aboutHeading": "Nutzen Sie Ihre kreative Energie",
|
"aboutHeading": "Nutzen Sie Ihre kreative Energie",
|
||||||
"toResolve": "Lösen",
|
"toResolve": "Lösen",
|
||||||
"add": "Hinzufügen",
|
"add": "Hinzufügen",
|
||||||
"loglevel": "Protokoll Stufe"
|
"loglevel": "Protokoll Stufe",
|
||||||
|
"selected": "Ausgewählt",
|
||||||
|
"beta": "Beta"
|
||||||
},
|
},
|
||||||
"gallery": {
|
"gallery": {
|
||||||
"galleryImageSize": "Bildgröße",
|
"galleryImageSize": "Bildgröße",
|
||||||
@ -86,7 +88,7 @@
|
|||||||
"noImagesInGallery": "Keine Bilder in der Galerie",
|
"noImagesInGallery": "Keine Bilder in der Galerie",
|
||||||
"loading": "Lade",
|
"loading": "Lade",
|
||||||
"deleteImage_one": "Lösche Bild",
|
"deleteImage_one": "Lösche Bild",
|
||||||
"deleteImage_other": "",
|
"deleteImage_other": "Lösche {{count}} Bilder",
|
||||||
"copy": "Kopieren",
|
"copy": "Kopieren",
|
||||||
"download": "Runterladen",
|
"download": "Runterladen",
|
||||||
"setCurrentImage": "Setze aktuelle Bild",
|
"setCurrentImage": "Setze aktuelle Bild",
|
||||||
@ -397,7 +399,14 @@
|
|||||||
"cancel": "Stornieren",
|
"cancel": "Stornieren",
|
||||||
"defaultSettingsSaved": "Standardeinstellungen gespeichert",
|
"defaultSettingsSaved": "Standardeinstellungen gespeichert",
|
||||||
"addModels": "Model hinzufügen",
|
"addModels": "Model hinzufügen",
|
||||||
"deleteModelImage": "Lösche Model Bild"
|
"deleteModelImage": "Lösche Model Bild",
|
||||||
|
"hfTokenInvalidErrorMessage": "Falscher oder fehlender HuggingFace Schlüssel.",
|
||||||
|
"huggingFaceRepoID": "HuggingFace Repo ID",
|
||||||
|
"hfToken": "HuggingFace Schlüssel",
|
||||||
|
"hfTokenInvalid": "Falscher oder fehlender HF Schlüssel",
|
||||||
|
"huggingFacePlaceholder": "besitzer/model-name",
|
||||||
|
"hfTokenSaved": "HF Schlüssel gespeichert",
|
||||||
|
"hfTokenUnableToVerify": "Konnte den HF Schlüssel nicht validieren"
|
||||||
},
|
},
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"images": "Bilder",
|
"images": "Bilder",
|
||||||
@ -686,7 +695,11 @@
|
|||||||
"hands": "Hände",
|
"hands": "Hände",
|
||||||
"dwOpenpose": "DW Openpose",
|
"dwOpenpose": "DW Openpose",
|
||||||
"dwOpenposeDescription": "Posenschätzung mit DW Openpose",
|
"dwOpenposeDescription": "Posenschätzung mit DW Openpose",
|
||||||
"selectCLIPVisionModel": "Wähle ein CLIP Vision Model aus"
|
"selectCLIPVisionModel": "Wähle ein CLIP Vision Model aus",
|
||||||
|
"ipAdapterMethod": "Methode",
|
||||||
|
"composition": "Nur Komposition",
|
||||||
|
"full": "Voll",
|
||||||
|
"style": "Nur Style"
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
@ -717,7 +730,6 @@
|
|||||||
"resume": "Wieder aufnehmen",
|
"resume": "Wieder aufnehmen",
|
||||||
"item": "Auftrag",
|
"item": "Auftrag",
|
||||||
"notReady": "Warteschlange noch nicht bereit",
|
"notReady": "Warteschlange noch nicht bereit",
|
||||||
"queueCountPrediction": "{{promptsCount}} Prompts × {{iterations}} Iterationen -> {{count}} Generationen",
|
|
||||||
"clearQueueAlertDialog": "\"Die Warteschlange leeren\" stoppt den aktuellen Prozess und leert die Warteschlange komplett.",
|
"clearQueueAlertDialog": "\"Die Warteschlange leeren\" stoppt den aktuellen Prozess und leert die Warteschlange komplett.",
|
||||||
"completedIn": "Fertig in",
|
"completedIn": "Fertig in",
|
||||||
"cancelBatchSucceeded": "Stapel abgebrochen",
|
"cancelBatchSucceeded": "Stapel abgebrochen",
|
||||||
|
@ -142,8 +142,11 @@
|
|||||||
"blue": "Blue",
|
"blue": "Blue",
|
||||||
"alpha": "Alpha",
|
"alpha": "Alpha",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"viewer": "Viewer",
|
"tab": "Tab",
|
||||||
"tab": "Tab"
|
"viewing": "Viewing",
|
||||||
|
"viewingDesc": "Review images in a large gallery view",
|
||||||
|
"editing": "Editing",
|
||||||
|
"editingDesc": "Edit on the Control Layers canvas"
|
||||||
},
|
},
|
||||||
"controlnet": {
|
"controlnet": {
|
||||||
"controlAdapter_one": "Control Adapter",
|
"controlAdapter_one": "Control Adapter",
|
||||||
@ -258,7 +261,6 @@
|
|||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
"queueFront": "Add to Front of Queue",
|
"queueFront": "Add to Front of Queue",
|
||||||
"queueBack": "Add to Queue",
|
"queueBack": "Add to Queue",
|
||||||
"queueCountPrediction": "{{promptsCount}} prompts \u00d7 {{iterations}} iterations -> {{count}} generations",
|
|
||||||
"queueEmpty": "Queue Empty",
|
"queueEmpty": "Queue Empty",
|
||||||
"enqueueing": "Queueing Batch",
|
"enqueueing": "Queueing Batch",
|
||||||
"resume": "Resume",
|
"resume": "Resume",
|
||||||
@ -311,7 +313,13 @@
|
|||||||
"batchFailedToQueue": "Failed to Queue Batch",
|
"batchFailedToQueue": "Failed to Queue Batch",
|
||||||
"graphQueued": "Graph queued",
|
"graphQueued": "Graph queued",
|
||||||
"graphFailedToQueue": "Failed to queue graph",
|
"graphFailedToQueue": "Failed to queue graph",
|
||||||
"openQueue": "Open Queue"
|
"openQueue": "Open Queue",
|
||||||
|
"prompts_one": "Prompt",
|
||||||
|
"prompts_other": "Prompts",
|
||||||
|
"iterations_one": "Iteration",
|
||||||
|
"iterations_other": "Iterations",
|
||||||
|
"generations_one": "Generation",
|
||||||
|
"generations_other": "Generations"
|
||||||
},
|
},
|
||||||
"invocationCache": {
|
"invocationCache": {
|
||||||
"invocationCache": "Invocation Cache",
|
"invocationCache": "Invocation Cache",
|
||||||
@ -364,8 +372,7 @@
|
|||||||
"bulkDownloadRequestFailed": "Problem Preparing Download",
|
"bulkDownloadRequestFailed": "Problem Preparing Download",
|
||||||
"bulkDownloadFailed": "Download Failed",
|
"bulkDownloadFailed": "Download Failed",
|
||||||
"problemDeletingImages": "Problem Deleting Images",
|
"problemDeletingImages": "Problem Deleting Images",
|
||||||
"problemDeletingImagesDesc": "One or more images could not be deleted",
|
"problemDeletingImagesDesc": "One or more images could not be deleted"
|
||||||
"switchTo": "Switch to {{ tab }} (Z)"
|
|
||||||
},
|
},
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"searchHotkeys": "Search Hotkeys",
|
"searchHotkeys": "Search Hotkeys",
|
||||||
@ -589,13 +596,9 @@
|
|||||||
"desc": "Upscale the current image",
|
"desc": "Upscale the current image",
|
||||||
"title": "Upscale"
|
"title": "Upscale"
|
||||||
},
|
},
|
||||||
"backToEditor": {
|
"toggleViewer": {
|
||||||
"desc": "Closes the Image Viewer and shows the Editor View (Text to Image tab only)",
|
"desc": "Switches between the Image Viewer and workspace for the current tab.",
|
||||||
"title": "Back to Editor"
|
"title": "Toggle Image Viewer"
|
||||||
},
|
|
||||||
"openImageViewer": {
|
|
||||||
"desc": "Opens the Image Viewer (Text to Image tab only)",
|
|
||||||
"title": "Open Image Viewer"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@ -771,6 +774,7 @@
|
|||||||
"cannotConnectOutputToOutput": "Cannot connect output to output",
|
"cannotConnectOutputToOutput": "Cannot connect output to output",
|
||||||
"cannotConnectToSelf": "Cannot connect to self",
|
"cannotConnectToSelf": "Cannot connect to self",
|
||||||
"cannotDuplicateConnection": "Cannot create duplicate connections",
|
"cannotDuplicateConnection": "Cannot create duplicate connections",
|
||||||
|
"cannotMixAndMatchCollectionItemTypes": "Cannot mix and match collection item types",
|
||||||
"nodePack": "Node pack",
|
"nodePack": "Node pack",
|
||||||
"collection": "Collection",
|
"collection": "Collection",
|
||||||
"collectionFieldType": "{{name}} Collection",
|
"collectionFieldType": "{{name}} Collection",
|
||||||
@ -876,6 +880,7 @@
|
|||||||
"versionUnknown": " Version Unknown",
|
"versionUnknown": " Version Unknown",
|
||||||
"workflow": "Workflow",
|
"workflow": "Workflow",
|
||||||
"graph": "Graph",
|
"graph": "Graph",
|
||||||
|
"noGraph": "No Graph",
|
||||||
"workflowAuthor": "Author",
|
"workflowAuthor": "Author",
|
||||||
"workflowContact": "Contact",
|
"workflowContact": "Contact",
|
||||||
"workflowDescription": "Short Description",
|
"workflowDescription": "Short Description",
|
||||||
@ -936,17 +941,30 @@
|
|||||||
"noModelSelected": "No model selected",
|
"noModelSelected": "No model selected",
|
||||||
"noPrompts": "No prompts generated",
|
"noPrompts": "No prompts generated",
|
||||||
"noNodesInGraph": "No nodes in graph",
|
"noNodesInGraph": "No nodes in graph",
|
||||||
"systemDisconnected": "System disconnected"
|
"systemDisconnected": "System disconnected",
|
||||||
|
"layer": {
|
||||||
|
"initialImageNoImageSelected": "no initial image selected",
|
||||||
|
"controlAdapterNoModelSelected": "no Control Adapter model selected",
|
||||||
|
"controlAdapterIncompatibleBaseModel": "incompatible Control Adapter base model",
|
||||||
|
"controlAdapterNoImageSelected": "no Control Adapter image selected",
|
||||||
|
"controlAdapterImageNotProcessed": "Control Adapter image not processed",
|
||||||
|
"t2iAdapterIncompatibleDimensions": "T2I Adapter requires image dimension to be multiples of 64",
|
||||||
|
"ipAdapterNoModelSelected": "no IP adapter selected",
|
||||||
|
"ipAdapterIncompatibleBaseModel": "incompatible IP Adapter base model",
|
||||||
|
"ipAdapterNoImageSelected": "no IP Adapter image selected",
|
||||||
|
"rgNoPromptsOrIPAdapters": "no text prompts or IP Adapters",
|
||||||
|
"rgNoRegion": "no region selected"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maskBlur": "Mask Blur",
|
"maskBlur": "Mask Blur",
|
||||||
"negativePromptPlaceholder": "Negative Prompt",
|
"negativePromptPlaceholder": "Negative Prompt",
|
||||||
|
"globalNegativePromptPlaceholder": "Global Negative Prompt",
|
||||||
"noiseThreshold": "Noise Threshold",
|
"noiseThreshold": "Noise Threshold",
|
||||||
"patchmatchDownScaleSize": "Downscale",
|
"patchmatchDownScaleSize": "Downscale",
|
||||||
"perlinNoise": "Perlin Noise",
|
"perlinNoise": "Perlin Noise",
|
||||||
"positivePromptPlaceholder": "Positive Prompt",
|
"positivePromptPlaceholder": "Positive Prompt",
|
||||||
|
"globalPositivePromptPlaceholder": "Global Positive Prompt",
|
||||||
"iterations": "Iterations",
|
"iterations": "Iterations",
|
||||||
"iterationsWithCount_one": "{{count}} Iteration",
|
|
||||||
"iterationsWithCount_other": "{{count}} Iterations",
|
|
||||||
"scale": "Scale",
|
"scale": "Scale",
|
||||||
"scaleBeforeProcessing": "Scale Before Processing",
|
"scaleBeforeProcessing": "Scale Before Processing",
|
||||||
"scaledHeight": "Scaled H",
|
"scaledHeight": "Scaled H",
|
||||||
@ -1548,8 +1566,6 @@
|
|||||||
"addIPAdapter": "Add $t(common.ipAdapter)",
|
"addIPAdapter": "Add $t(common.ipAdapter)",
|
||||||
"regionalGuidance": "Regional Guidance",
|
"regionalGuidance": "Regional Guidance",
|
||||||
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
|
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
|
||||||
"controlNetLayer": "$t(common.controlNet) $t(unifiedCanvas.layer)",
|
|
||||||
"ipAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer)",
|
|
||||||
"opacity": "Opacity",
|
"opacity": "Opacity",
|
||||||
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
|
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
|
||||||
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
|
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
|
||||||
@ -1560,7 +1576,9 @@
|
|||||||
"opacityFilter": "Opacity Filter",
|
"opacityFilter": "Opacity Filter",
|
||||||
"clearProcessor": "Clear Processor",
|
"clearProcessor": "Clear Processor",
|
||||||
"resetProcessor": "Reset Processor to Defaults",
|
"resetProcessor": "Reset Processor to Defaults",
|
||||||
"noLayersAdded": "No Layers Added"
|
"noLayersAdded": "No Layers Added",
|
||||||
|
"layers_one": "Layer",
|
||||||
|
"layers_other": "Layers"
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"tabs": {
|
"tabs": {
|
||||||
|
@ -25,7 +25,24 @@
|
|||||||
"areYouSure": "¿Estas seguro?",
|
"areYouSure": "¿Estas seguro?",
|
||||||
"batch": "Administrador de lotes",
|
"batch": "Administrador de lotes",
|
||||||
"modelManager": "Administrador de modelos",
|
"modelManager": "Administrador de modelos",
|
||||||
"communityLabel": "Comunidad"
|
"communityLabel": "Comunidad",
|
||||||
|
"direction": "Dirección",
|
||||||
|
"ai": "Ia",
|
||||||
|
"add": "Añadir",
|
||||||
|
"auto": "Automático",
|
||||||
|
"copyError": "Error $t(gallery.copy)",
|
||||||
|
"details": "Detalles",
|
||||||
|
"or": "o",
|
||||||
|
"checkpoint": "Punto de control",
|
||||||
|
"controlNet": "ControlNet",
|
||||||
|
"aboutHeading": "Sea dueño de su poder creativo",
|
||||||
|
"advanced": "Avanzado",
|
||||||
|
"data": "Fecha",
|
||||||
|
"delete": "Borrar",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"beta": "Beta",
|
||||||
|
"on": "En",
|
||||||
|
"aboutDesc": "¿Utilizas Invoke para trabajar? Mira aquí:"
|
||||||
},
|
},
|
||||||
"gallery": {
|
"gallery": {
|
||||||
"galleryImageSize": "Tamaño de la imagen",
|
"galleryImageSize": "Tamaño de la imagen",
|
||||||
@ -443,7 +460,13 @@
|
|||||||
"previousImage": "Imagen anterior",
|
"previousImage": "Imagen anterior",
|
||||||
"nextImage": "Siguiente imagen",
|
"nextImage": "Siguiente imagen",
|
||||||
"showOptionsPanel": "Mostrar el panel lateral",
|
"showOptionsPanel": "Mostrar el panel lateral",
|
||||||
"menu": "Menú"
|
"menu": "Menú",
|
||||||
|
"showGalleryPanel": "Mostrar panel de galería",
|
||||||
|
"loadMore": "Cargar más",
|
||||||
|
"about": "Acerca de",
|
||||||
|
"createIssue": "Crear un problema",
|
||||||
|
"resetUI": "Interfaz de usuario $t(accessibility.reset)",
|
||||||
|
"mode": "Modo"
|
||||||
},
|
},
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"zoomInNodes": "Acercar",
|
"zoomInNodes": "Acercar",
|
||||||
@ -456,5 +479,68 @@
|
|||||||
"reloadNodeTemplates": "Recargar las plantillas de nodos",
|
"reloadNodeTemplates": "Recargar las plantillas de nodos",
|
||||||
"loadWorkflow": "Cargar el flujo de trabajo",
|
"loadWorkflow": "Cargar el flujo de trabajo",
|
||||||
"downloadWorkflow": "Descargar el flujo de trabajo en un archivo JSON"
|
"downloadWorkflow": "Descargar el flujo de trabajo en un archivo JSON"
|
||||||
|
},
|
||||||
|
"boards": {
|
||||||
|
"autoAddBoard": "Agregar panel automáticamente",
|
||||||
|
"changeBoard": "Cambiar el panel",
|
||||||
|
"clearSearch": "Borrar la búsqueda",
|
||||||
|
"deleteBoard": "Borrar el panel",
|
||||||
|
"selectBoard": "Seleccionar un panel",
|
||||||
|
"uncategorized": "Sin categoría",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"addBoard": "Agregar un panel",
|
||||||
|
"movingImagesToBoard_one": "Moviendo {{count}} imagen al panel:",
|
||||||
|
"movingImagesToBoard_many": "Moviendo {{count}} imágenes al panel:",
|
||||||
|
"movingImagesToBoard_other": "Moviendo {{count}} imágenes al panel:",
|
||||||
|
"bottomMessage": "Al eliminar este panel y las imágenes que contiene, se restablecerán las funciones que los estén utilizando actualmente.",
|
||||||
|
"deleteBoardAndImages": "Borrar el panel y las imágenes",
|
||||||
|
"loading": "Cargando...",
|
||||||
|
"deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar",
|
||||||
|
"move": "Mover",
|
||||||
|
"menuItemAutoAdd": "Agregar automáticamente a este panel",
|
||||||
|
"searchBoard": "Buscando paneles…",
|
||||||
|
"topMessage": "Este panel contiene imágenes utilizadas en las siguientes funciones:",
|
||||||
|
"downloadBoard": "Descargar panel",
|
||||||
|
"deleteBoardOnly": "Borrar solo el panel",
|
||||||
|
"myBoard": "Mi panel",
|
||||||
|
"noMatching": "No hay paneles que coincidan"
|
||||||
|
},
|
||||||
|
"accordions": {
|
||||||
|
"compositing": {
|
||||||
|
"title": "Composición",
|
||||||
|
"infillTab": "Relleno"
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
"title": "Generación"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"title": "Imagen"
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"title": "Control"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"options": "$t(accordions.advanced.title) opciones",
|
||||||
|
"title": "Avanzado"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"tabs": {
|
||||||
|
"generationTab": "$t(ui.tabs.generation) $t(common.tab)",
|
||||||
|
"canvas": "Lienzo",
|
||||||
|
"generation": "Generación",
|
||||||
|
"queue": "Cola",
|
||||||
|
"queueTab": "$t(ui.tabs.queue) $t(common.tab)",
|
||||||
|
"workflows": "Flujos de trabajo",
|
||||||
|
"models": "Modelos",
|
||||||
|
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
||||||
|
"canvasTab": "$t(ui.tabs.canvas) $t(common.tab)",
|
||||||
|
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"controlLayers": {
|
||||||
|
"layers_one": "Capa",
|
||||||
|
"layers_many": "Capas",
|
||||||
|
"layers_other": "Capas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
"reportBugLabel": "Segnala un errore",
|
"reportBugLabel": "Segnala un errore",
|
||||||
"settingsLabel": "Impostazioni",
|
"settingsLabel": "Impostazioni",
|
||||||
"img2img": "Immagine a Immagine",
|
"img2img": "Immagine a Immagine",
|
||||||
"unifiedCanvas": "Tela unificata",
|
"unifiedCanvas": "Tela",
|
||||||
"nodes": "Flussi di lavoro",
|
"nodes": "Flussi di lavoro",
|
||||||
"upload": "Caricamento",
|
"upload": "Caricamento",
|
||||||
"load": "Carica",
|
"load": "Carica",
|
||||||
@ -74,7 +74,18 @@
|
|||||||
"file": "File",
|
"file": "File",
|
||||||
"toResolve": "Da risolvere",
|
"toResolve": "Da risolvere",
|
||||||
"add": "Aggiungi",
|
"add": "Aggiungi",
|
||||||
"loglevel": "Livello di log"
|
"loglevel": "Livello di log",
|
||||||
|
"beta": "Beta",
|
||||||
|
"positivePrompt": "Prompt positivo",
|
||||||
|
"negativePrompt": "Prompt negativo",
|
||||||
|
"selected": "Selezionato",
|
||||||
|
"goTo": "Vai a",
|
||||||
|
"editor": "Editor",
|
||||||
|
"tab": "Scheda",
|
||||||
|
"viewing": "Visualizza",
|
||||||
|
"viewingDesc": "Rivedi le immagini in un'ampia vista della galleria",
|
||||||
|
"editing": "Modifica",
|
||||||
|
"editingDesc": "Modifica nell'area Livelli di controllo"
|
||||||
},
|
},
|
||||||
"gallery": {
|
"gallery": {
|
||||||
"galleryImageSize": "Dimensione dell'immagine",
|
"galleryImageSize": "Dimensione dell'immagine",
|
||||||
@ -180,8 +191,8 @@
|
|||||||
"desc": "Mostra le informazioni sui metadati dell'immagine corrente"
|
"desc": "Mostra le informazioni sui metadati dell'immagine corrente"
|
||||||
},
|
},
|
||||||
"sendToImageToImage": {
|
"sendToImageToImage": {
|
||||||
"title": "Invia a Immagine a Immagine",
|
"title": "Invia a Generazione da immagine",
|
||||||
"desc": "Invia l'immagine corrente a da Immagine a Immagine"
|
"desc": "Invia l'immagine corrente a Generazione da immagine"
|
||||||
},
|
},
|
||||||
"deleteImage": {
|
"deleteImage": {
|
||||||
"title": "Elimina immagine",
|
"title": "Elimina immagine",
|
||||||
@ -334,6 +345,10 @@
|
|||||||
"remixImage": {
|
"remixImage": {
|
||||||
"desc": "Utilizza tutti i parametri tranne il seme dell'immagine corrente",
|
"desc": "Utilizza tutti i parametri tranne il seme dell'immagine corrente",
|
||||||
"title": "Remixa l'immagine"
|
"title": "Remixa l'immagine"
|
||||||
|
},
|
||||||
|
"toggleViewer": {
|
||||||
|
"title": "Attiva/disattiva il visualizzatore di immagini",
|
||||||
|
"desc": "Passa dal Visualizzatore immagini all'area di lavoro per la scheda corrente."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modelManager": {
|
"modelManager": {
|
||||||
@ -471,8 +486,8 @@
|
|||||||
"scaledHeight": "Altezza ridimensionata",
|
"scaledHeight": "Altezza ridimensionata",
|
||||||
"infillMethod": "Metodo di riempimento",
|
"infillMethod": "Metodo di riempimento",
|
||||||
"tileSize": "Dimensione piastrella",
|
"tileSize": "Dimensione piastrella",
|
||||||
"sendToImg2Img": "Invia a Immagine a Immagine",
|
"sendToImg2Img": "Invia a Generazione da immagine",
|
||||||
"sendToUnifiedCanvas": "Invia a Tela Unificata",
|
"sendToUnifiedCanvas": "Invia alla Tela",
|
||||||
"downloadImage": "Scarica l'immagine",
|
"downloadImage": "Scarica l'immagine",
|
||||||
"usePrompt": "Usa Prompt",
|
"usePrompt": "Usa Prompt",
|
||||||
"useSeed": "Usa Seme",
|
"useSeed": "Usa Seme",
|
||||||
@ -508,13 +523,11 @@
|
|||||||
"incompatibleBaseModelForControlAdapter": "Il modello dell'adattatore di controllo #{{number}} non è compatibile con il modello principale.",
|
"incompatibleBaseModelForControlAdapter": "Il modello dell'adattatore di controllo #{{number}} non è compatibile con il modello principale.",
|
||||||
"missingNodeTemplate": "Modello di nodo mancante",
|
"missingNodeTemplate": "Modello di nodo mancante",
|
||||||
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} ingresso mancante",
|
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} ingresso mancante",
|
||||||
"missingFieldTemplate": "Modello di campo mancante"
|
"missingFieldTemplate": "Modello di campo mancante",
|
||||||
|
"imageNotProcessedForControlAdapter": "L'immagine dell'adattatore di controllo #{{number}} non è stata elaborata"
|
||||||
},
|
},
|
||||||
"useCpuNoise": "Usa la CPU per generare rumore",
|
"useCpuNoise": "Usa la CPU per generare rumore",
|
||||||
"iterations": "Iterazioni",
|
"iterations": "Iterazioni",
|
||||||
"iterationsWithCount_one": "{{count}} Iterazione",
|
|
||||||
"iterationsWithCount_many": "{{count}} Iterazioni",
|
|
||||||
"iterationsWithCount_other": "{{count}} Iterazioni",
|
|
||||||
"isAllowedToUpscale": {
|
"isAllowedToUpscale": {
|
||||||
"useX2Model": "L'immagine è troppo grande per l'ampliamento con il modello x4, utilizza il modello x2",
|
"useX2Model": "L'immagine è troppo grande per l'ampliamento con il modello x4, utilizza il modello x2",
|
||||||
"tooLarge": "L'immagine è troppo grande per l'ampliamento, seleziona un'immagine più piccola"
|
"tooLarge": "L'immagine è troppo grande per l'ampliamento, seleziona un'immagine più piccola"
|
||||||
@ -534,7 +547,10 @@
|
|||||||
"infillMosaicMinColor": "Colore minimo",
|
"infillMosaicMinColor": "Colore minimo",
|
||||||
"infillMosaicMaxColor": "Colore massimo",
|
"infillMosaicMaxColor": "Colore massimo",
|
||||||
"infillMosaicTileHeight": "Altezza piastrella",
|
"infillMosaicTileHeight": "Altezza piastrella",
|
||||||
"infillColorValue": "Colore di riempimento"
|
"infillColorValue": "Colore di riempimento",
|
||||||
|
"globalSettings": "Impostazioni globali",
|
||||||
|
"globalPositivePromptPlaceholder": "Prompt positivo globale",
|
||||||
|
"globalNegativePromptPlaceholder": "Prompt negativo globale"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"models": "Modelli",
|
"models": "Modelli",
|
||||||
@ -559,7 +575,7 @@
|
|||||||
"intermediatesCleared_one": "Cancellata {{count}} immagine intermedia",
|
"intermediatesCleared_one": "Cancellata {{count}} immagine intermedia",
|
||||||
"intermediatesCleared_many": "Cancellate {{count}} immagini intermedie",
|
"intermediatesCleared_many": "Cancellate {{count}} immagini intermedie",
|
||||||
"intermediatesCleared_other": "Cancellate {{count}} immagini intermedie",
|
"intermediatesCleared_other": "Cancellate {{count}} immagini intermedie",
|
||||||
"clearIntermediatesDesc1": "La cancellazione delle immagini intermedie ripristinerà lo stato di Tela Unificata e ControlNet.",
|
"clearIntermediatesDesc1": "La cancellazione delle immagini intermedie ripristinerà lo stato della Tela e degli Adattatori di Controllo.",
|
||||||
"intermediatesClearedFailed": "Problema con la cancellazione delle immagini intermedie",
|
"intermediatesClearedFailed": "Problema con la cancellazione delle immagini intermedie",
|
||||||
"clearIntermediatesWithCount_one": "Cancella {{count}} immagine intermedia",
|
"clearIntermediatesWithCount_one": "Cancella {{count}} immagine intermedia",
|
||||||
"clearIntermediatesWithCount_many": "Cancella {{count}} immagini intermedie",
|
"clearIntermediatesWithCount_many": "Cancella {{count}} immagini intermedie",
|
||||||
@ -575,8 +591,8 @@
|
|||||||
"imageCopied": "Immagine copiata",
|
"imageCopied": "Immagine copiata",
|
||||||
"imageNotLoadedDesc": "Impossibile trovare l'immagine",
|
"imageNotLoadedDesc": "Impossibile trovare l'immagine",
|
||||||
"canvasMerged": "Tela unita",
|
"canvasMerged": "Tela unita",
|
||||||
"sentToImageToImage": "Inviato a Immagine a Immagine",
|
"sentToImageToImage": "Inviato a Generazione da immagine",
|
||||||
"sentToUnifiedCanvas": "Inviato a Tela Unificata",
|
"sentToUnifiedCanvas": "Inviato alla Tela",
|
||||||
"parametersNotSet": "Parametri non impostati",
|
"parametersNotSet": "Parametri non impostati",
|
||||||
"metadataLoadFailed": "Impossibile caricare i metadati",
|
"metadataLoadFailed": "Impossibile caricare i metadati",
|
||||||
"serverError": "Errore del Server",
|
"serverError": "Errore del Server",
|
||||||
@ -795,7 +811,7 @@
|
|||||||
"float": "In virgola mobile",
|
"float": "In virgola mobile",
|
||||||
"currentImageDescription": "Visualizza l'immagine corrente nell'editor dei nodi",
|
"currentImageDescription": "Visualizza l'immagine corrente nell'editor dei nodi",
|
||||||
"fieldTypesMustMatch": "I tipi di campo devono corrispondere",
|
"fieldTypesMustMatch": "I tipi di campo devono corrispondere",
|
||||||
"edge": "Bordo",
|
"edge": "Collegamento",
|
||||||
"currentImage": "Immagine corrente",
|
"currentImage": "Immagine corrente",
|
||||||
"integer": "Numero Intero",
|
"integer": "Numero Intero",
|
||||||
"inputMayOnlyHaveOneConnection": "L'ingresso può avere solo una connessione",
|
"inputMayOnlyHaveOneConnection": "L'ingresso può avere solo una connessione",
|
||||||
@ -845,7 +861,9 @@
|
|||||||
"resetToDefaultValue": "Ripristina il valore predefinito",
|
"resetToDefaultValue": "Ripristina il valore predefinito",
|
||||||
"noFieldsViewMode": "Questo flusso di lavoro non ha campi selezionati da visualizzare. Visualizza il flusso di lavoro completo per configurare i valori.",
|
"noFieldsViewMode": "Questo flusso di lavoro non ha campi selezionati da visualizzare. Visualizza il flusso di lavoro completo per configurare i valori.",
|
||||||
"edit": "Modifica",
|
"edit": "Modifica",
|
||||||
"graph": "Grafico"
|
"graph": "Grafico",
|
||||||
|
"showEdgeLabelsHelp": "Mostra etichette sui collegamenti, che indicano i nodi collegati",
|
||||||
|
"showEdgeLabels": "Mostra le etichette del collegamento"
|
||||||
},
|
},
|
||||||
"boards": {
|
"boards": {
|
||||||
"autoAddBoard": "Aggiungi automaticamente bacheca",
|
"autoAddBoard": "Aggiungi automaticamente bacheca",
|
||||||
@ -922,7 +940,7 @@
|
|||||||
"colorMapTileSize": "Dimensione piastrella",
|
"colorMapTileSize": "Dimensione piastrella",
|
||||||
"mediapipeFaceDescription": "Rilevamento dei volti tramite Mediapipe",
|
"mediapipeFaceDescription": "Rilevamento dei volti tramite Mediapipe",
|
||||||
"hedDescription": "Rilevamento dei bordi nidificati olisticamente",
|
"hedDescription": "Rilevamento dei bordi nidificati olisticamente",
|
||||||
"setControlImageDimensions": "Imposta le dimensioni dell'immagine di controllo su L/A",
|
"setControlImageDimensions": "Copia le dimensioni in L/A (ottimizza per il modello)",
|
||||||
"maxFaces": "Numero massimo di volti",
|
"maxFaces": "Numero massimo di volti",
|
||||||
"addT2IAdapter": "Aggiungi $t(common.t2iAdapter)",
|
"addT2IAdapter": "Aggiungi $t(common.t2iAdapter)",
|
||||||
"addControlNet": "Aggiungi $t(common.controlNet)",
|
"addControlNet": "Aggiungi $t(common.controlNet)",
|
||||||
@ -951,12 +969,17 @@
|
|||||||
"mediapipeFace": "Mediapipe Volto",
|
"mediapipeFace": "Mediapipe Volto",
|
||||||
"ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))",
|
"ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))",
|
||||||
"t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))",
|
"t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))",
|
||||||
"selectCLIPVisionModel": "Seleziona un modello CLIP Vision"
|
"selectCLIPVisionModel": "Seleziona un modello CLIP Vision",
|
||||||
|
"ipAdapterMethod": "Metodo",
|
||||||
|
"full": "Completo",
|
||||||
|
"composition": "Solo la composizione",
|
||||||
|
"style": "Solo lo stile",
|
||||||
|
"beginEndStepPercentShort": "Inizio/Fine %",
|
||||||
|
"setControlImageDimensionsForce": "Copia le dimensioni in L/A (ignora il modello)"
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"queueFront": "Aggiungi all'inizio della coda",
|
"queueFront": "Aggiungi all'inizio della coda",
|
||||||
"queueBack": "Aggiungi alla coda",
|
"queueBack": "Aggiungi alla coda",
|
||||||
"queueCountPrediction": "{{promptsCount}} prompt × {{iterations}} iterazioni -> {{count}} generazioni",
|
|
||||||
"queue": "Coda",
|
"queue": "Coda",
|
||||||
"status": "Stato",
|
"status": "Stato",
|
||||||
"pruneSucceeded": "Rimossi {{item_count}} elementi completati dalla coda",
|
"pruneSucceeded": "Rimossi {{item_count}} elementi completati dalla coda",
|
||||||
@ -993,7 +1016,7 @@
|
|||||||
"cancelBatchSucceeded": "Lotto annullato",
|
"cancelBatchSucceeded": "Lotto annullato",
|
||||||
"clearTooltip": "Annulla e cancella tutti gli elementi",
|
"clearTooltip": "Annulla e cancella tutti gli elementi",
|
||||||
"current": "Attuale",
|
"current": "Attuale",
|
||||||
"pauseTooltip": "Sospende l'elaborazione",
|
"pauseTooltip": "Sospendi l'elaborazione",
|
||||||
"failed": "Falliti",
|
"failed": "Falliti",
|
||||||
"cancelItem": "Annulla l'elemento",
|
"cancelItem": "Annulla l'elemento",
|
||||||
"next": "Prossimo",
|
"next": "Prossimo",
|
||||||
@ -1394,6 +1417,12 @@
|
|||||||
"paragraphs": [
|
"paragraphs": [
|
||||||
"La dimensione del bordo del passaggio di coerenza."
|
"La dimensione del bordo del passaggio di coerenza."
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"ipAdapterMethod": {
|
||||||
|
"heading": "Metodo",
|
||||||
|
"paragraphs": [
|
||||||
|
"Metodo con cui applicare l'adattatore IP corrente."
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sdxl": {
|
"sdxl": {
|
||||||
@ -1522,5 +1551,56 @@
|
|||||||
"compatibleEmbeddings": "Incorporamenti compatibili",
|
"compatibleEmbeddings": "Incorporamenti compatibili",
|
||||||
"addPromptTrigger": "Aggiungi Trigger nel prompt",
|
"addPromptTrigger": "Aggiungi Trigger nel prompt",
|
||||||
"noMatchingTriggers": "Nessun Trigger corrispondente"
|
"noMatchingTriggers": "Nessun Trigger corrispondente"
|
||||||
|
},
|
||||||
|
"controlLayers": {
|
||||||
|
"opacityFilter": "Filtro opacità",
|
||||||
|
"deleteAll": "Cancella tutto",
|
||||||
|
"addLayer": "Aggiungi Livello",
|
||||||
|
"moveToFront": "Sposta in primo piano",
|
||||||
|
"moveToBack": "Sposta in fondo",
|
||||||
|
"moveForward": "Sposta avanti",
|
||||||
|
"moveBackward": "Sposta indietro",
|
||||||
|
"brushSize": "Dimensioni del pennello",
|
||||||
|
"globalMaskOpacity": "Opacità globale della maschera",
|
||||||
|
"autoNegative": "Auto Negativo",
|
||||||
|
"toggleVisibility": "Attiva/disattiva la visibilità dei livelli",
|
||||||
|
"deletePrompt": "Cancella il prompt",
|
||||||
|
"debugLayers": "Debug dei Livelli",
|
||||||
|
"rectangle": "Rettangolo",
|
||||||
|
"maskPreviewColor": "Colore anteprima maschera",
|
||||||
|
"addPositivePrompt": "Aggiungi $t(common.positivePrompt)",
|
||||||
|
"addNegativePrompt": "Aggiungi $t(common.negativePrompt)",
|
||||||
|
"addIPAdapter": "Aggiungi $t(common.ipAdapter)",
|
||||||
|
"regionalGuidance": "Guida regionale",
|
||||||
|
"regionalGuidanceLayer": "$t(unifiedCanvas.layer) $t(controlLayers.regionalGuidance)",
|
||||||
|
"opacity": "Opacità",
|
||||||
|
"globalControlAdapter": "$t(controlnet.controlAdapter_one) Globale",
|
||||||
|
"globalControlAdapterLayer": "$t(controlnet.controlAdapter_one) - $t(unifiedCanvas.layer) Globale",
|
||||||
|
"globalIPAdapter": "$t(common.ipAdapter) Globale",
|
||||||
|
"globalIPAdapterLayer": "$t(common.ipAdapter) - $t(unifiedCanvas.layer) Globale",
|
||||||
|
"globalInitialImage": "Immagine iniziale",
|
||||||
|
"globalInitialImageLayer": "$t(controlLayers.globalInitialImage) - $t(unifiedCanvas.layer) Globale",
|
||||||
|
"clearProcessor": "Cancella processore",
|
||||||
|
"resetProcessor": "Ripristina il processore alle impostazioni predefinite",
|
||||||
|
"noLayersAdded": "Nessun livello aggiunto",
|
||||||
|
"resetRegion": "Reimposta la regione",
|
||||||
|
"controlLayers": "Livelli di controllo",
|
||||||
|
"layers_one": "Livello",
|
||||||
|
"layers_many": "Livelli",
|
||||||
|
"layers_other": "Livelli"
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"tabs": {
|
||||||
|
"generation": "Generazione",
|
||||||
|
"generationTab": "$t(ui.tabs.generation) $t(common.tab)",
|
||||||
|
"canvas": "Tela",
|
||||||
|
"canvasTab": "$t(ui.tabs.canvas) $t(common.tab)",
|
||||||
|
"workflows": "Flussi di lavoro",
|
||||||
|
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
|
||||||
|
"models": "Modelli",
|
||||||
|
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
||||||
|
"queue": "Coda",
|
||||||
|
"queueTab": "$t(ui.tabs.queue) $t(common.tab)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -570,7 +570,6 @@
|
|||||||
"pauseSucceeded": "処理が一時停止されました",
|
"pauseSucceeded": "処理が一時停止されました",
|
||||||
"queueFront": "キューの先頭へ追加",
|
"queueFront": "キューの先頭へ追加",
|
||||||
"queueBack": "キューに追加",
|
"queueBack": "キューに追加",
|
||||||
"queueCountPrediction": "{{promptsCount}} プロンプト × {{iterations}} イテレーション -> {{count}} 枚生成",
|
|
||||||
"pause": "一時停止",
|
"pause": "一時停止",
|
||||||
"queue": "キュー",
|
"queue": "キュー",
|
||||||
"pauseTooltip": "処理を一時停止",
|
"pauseTooltip": "処理を一時停止",
|
||||||
|
@ -505,7 +505,6 @@
|
|||||||
"completed": "완성된",
|
"completed": "완성된",
|
||||||
"queueBack": "Queue에 추가",
|
"queueBack": "Queue에 추가",
|
||||||
"cancelFailed": "항목 취소 중 발생한 문제",
|
"cancelFailed": "항목 취소 중 발생한 문제",
|
||||||
"queueCountPrediction": "Queue에 {{predicted}} 추가",
|
|
||||||
"batchQueued": "Batch Queued",
|
"batchQueued": "Batch Queued",
|
||||||
"pauseFailed": "프로세서 중지 중 발생한 문제",
|
"pauseFailed": "프로세서 중지 중 발생한 문제",
|
||||||
"clearFailed": "Queue 제거 중 발생한 문제",
|
"clearFailed": "Queue 제거 중 발생한 문제",
|
||||||
|
@ -383,8 +383,6 @@
|
|||||||
"useCpuNoise": "Gebruik CPU-ruis",
|
"useCpuNoise": "Gebruik CPU-ruis",
|
||||||
"imageActions": "Afbeeldingshandeling",
|
"imageActions": "Afbeeldingshandeling",
|
||||||
"iterations": "Iteraties",
|
"iterations": "Iteraties",
|
||||||
"iterationsWithCount_one": "{{count}} iteratie",
|
|
||||||
"iterationsWithCount_other": "{{count}} iteraties",
|
|
||||||
"coherenceMode": "Modus"
|
"coherenceMode": "Modus"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@ -940,7 +938,6 @@
|
|||||||
"completed": "Voltooid",
|
"completed": "Voltooid",
|
||||||
"queueBack": "Voeg toe aan wachtrij",
|
"queueBack": "Voeg toe aan wachtrij",
|
||||||
"cancelFailed": "Fout bij annuleren onderdeel",
|
"cancelFailed": "Fout bij annuleren onderdeel",
|
||||||
"queueCountPrediction": "Voeg {{predicted}} toe aan wachtrij",
|
|
||||||
"batchQueued": "Reeks in wachtrij geplaatst",
|
"batchQueued": "Reeks in wachtrij geplaatst",
|
||||||
"pauseFailed": "Fout bij onderbreken verwerker",
|
"pauseFailed": "Fout bij onderbreken verwerker",
|
||||||
"clearFailed": "Fout bij wissen van wachtrij",
|
"clearFailed": "Fout bij wissen van wachtrij",
|
||||||
|
@ -76,7 +76,18 @@
|
|||||||
"localSystem": "Локальная система",
|
"localSystem": "Локальная система",
|
||||||
"aboutDesc": "Используя Invoke для работы? Проверьте это:",
|
"aboutDesc": "Используя Invoke для работы? Проверьте это:",
|
||||||
"add": "Добавить",
|
"add": "Добавить",
|
||||||
"loglevel": "Уровень логов"
|
"loglevel": "Уровень логов",
|
||||||
|
"beta": "Бета",
|
||||||
|
"selected": "Выбрано",
|
||||||
|
"positivePrompt": "Позитивный запрос",
|
||||||
|
"negativePrompt": "Негативный запрос",
|
||||||
|
"editor": "Редактор",
|
||||||
|
"goTo": "Перейти к",
|
||||||
|
"tab": "Вкладка",
|
||||||
|
"viewing": "Просмотр",
|
||||||
|
"editing": "Редактирование",
|
||||||
|
"viewingDesc": "Просмотр изображений в режиме большой галереи",
|
||||||
|
"editingDesc": "Редактировать на холсте слоёв управления"
|
||||||
},
|
},
|
||||||
"gallery": {
|
"gallery": {
|
||||||
"galleryImageSize": "Размер изображений",
|
"galleryImageSize": "Размер изображений",
|
||||||
@ -87,8 +98,8 @@
|
|||||||
"deleteImagePermanent": "Удаленные изображения невозможно восстановить.",
|
"deleteImagePermanent": "Удаленные изображения невозможно восстановить.",
|
||||||
"deleteImageBin": "Удаленные изображения будут отправлены в корзину вашей операционной системы.",
|
"deleteImageBin": "Удаленные изображения будут отправлены в корзину вашей операционной системы.",
|
||||||
"deleteImage_one": "Удалить изображение",
|
"deleteImage_one": "Удалить изображение",
|
||||||
"deleteImage_few": "",
|
"deleteImage_few": "Удалить {{count}} изображения",
|
||||||
"deleteImage_many": "",
|
"deleteImage_many": "Удалить {{count}} изображений",
|
||||||
"assets": "Ресурсы",
|
"assets": "Ресурсы",
|
||||||
"autoAssignBoardOnClick": "Авто-назначение доски по клику",
|
"autoAssignBoardOnClick": "Авто-назначение доски по клику",
|
||||||
"deleteSelection": "Удалить выделенное",
|
"deleteSelection": "Удалить выделенное",
|
||||||
@ -336,6 +347,10 @@
|
|||||||
"remixImage": {
|
"remixImage": {
|
||||||
"desc": "Используйте все параметры, кроме сида из текущего изображения",
|
"desc": "Используйте все параметры, кроме сида из текущего изображения",
|
||||||
"title": "Ремикс изображения"
|
"title": "Ремикс изображения"
|
||||||
|
},
|
||||||
|
"toggleViewer": {
|
||||||
|
"title": "Переключить просмотр изображений",
|
||||||
|
"desc": "Переключение между средством просмотра изображений и рабочей областью для текущей вкладки."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modelManager": {
|
"modelManager": {
|
||||||
@ -512,7 +527,8 @@
|
|||||||
"missingNodeTemplate": "Отсутствует шаблон узла",
|
"missingNodeTemplate": "Отсутствует шаблон узла",
|
||||||
"missingFieldTemplate": "Отсутствует шаблон поля",
|
"missingFieldTemplate": "Отсутствует шаблон поля",
|
||||||
"addingImagesTo": "Добавление изображений в",
|
"addingImagesTo": "Добавление изображений в",
|
||||||
"invoke": "Создать"
|
"invoke": "Создать",
|
||||||
|
"imageNotProcessedForControlAdapter": "Изображение адаптера контроля №{{number}} не обрабатывается"
|
||||||
},
|
},
|
||||||
"isAllowedToUpscale": {
|
"isAllowedToUpscale": {
|
||||||
"useX2Model": "Изображение слишком велико для увеличения с помощью модели x4. Используйте модель x2",
|
"useX2Model": "Изображение слишком велико для увеличения с помощью модели x4. Используйте модель x2",
|
||||||
@ -523,9 +539,6 @@
|
|||||||
"useCpuNoise": "Использовать шум CPU",
|
"useCpuNoise": "Использовать шум CPU",
|
||||||
"imageActions": "Действия с изображениями",
|
"imageActions": "Действия с изображениями",
|
||||||
"iterations": "Кол-во",
|
"iterations": "Кол-во",
|
||||||
"iterationsWithCount_one": "{{count}} Интеграция",
|
|
||||||
"iterationsWithCount_few": "{{count}} Итерации",
|
|
||||||
"iterationsWithCount_many": "{{count}} Итераций",
|
|
||||||
"useSize": "Использовать размер",
|
"useSize": "Использовать размер",
|
||||||
"coherenceMode": "Режим",
|
"coherenceMode": "Режим",
|
||||||
"aspect": "Соотношение",
|
"aspect": "Соотношение",
|
||||||
@ -541,7 +554,10 @@
|
|||||||
"infillMosaicTileHeight": "Высота плиток",
|
"infillMosaicTileHeight": "Высота плиток",
|
||||||
"infillMosaicMinColor": "Мин цвет",
|
"infillMosaicMinColor": "Мин цвет",
|
||||||
"infillMosaicMaxColor": "Макс цвет",
|
"infillMosaicMaxColor": "Макс цвет",
|
||||||
"infillColorValue": "Цвет заливки"
|
"infillColorValue": "Цвет заливки",
|
||||||
|
"globalSettings": "Глобальные настройки",
|
||||||
|
"globalNegativePromptPlaceholder": "Глобальный негативный запрос",
|
||||||
|
"globalPositivePromptPlaceholder": "Глобальный запрос"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"models": "Модели",
|
"models": "Модели",
|
||||||
@ -706,7 +722,9 @@
|
|||||||
"coherenceModeBoxBlur": "коробчатое размытие",
|
"coherenceModeBoxBlur": "коробчатое размытие",
|
||||||
"discardCurrent": "Отбросить текущее",
|
"discardCurrent": "Отбросить текущее",
|
||||||
"invertBrushSizeScrollDirection": "Инвертировать прокрутку для размера кисти",
|
"invertBrushSizeScrollDirection": "Инвертировать прокрутку для размера кисти",
|
||||||
"initialFitImageSize": "Подогнать размер изображения при перебросе"
|
"initialFitImageSize": "Подогнать размер изображения при перебросе",
|
||||||
|
"hideBoundingBox": "Скрыть ограничительную рамку",
|
||||||
|
"showBoundingBox": "Показать ограничительную рамку"
|
||||||
},
|
},
|
||||||
"accessibility": {
|
"accessibility": {
|
||||||
"uploadImage": "Загрузить изображение",
|
"uploadImage": "Загрузить изображение",
|
||||||
@ -849,7 +867,10 @@
|
|||||||
"editMode": "Открыть в редакторе узлов",
|
"editMode": "Открыть в редакторе узлов",
|
||||||
"resetToDefaultValue": "Сбросить к стандартному значкнию",
|
"resetToDefaultValue": "Сбросить к стандартному значкнию",
|
||||||
"edit": "Редактировать",
|
"edit": "Редактировать",
|
||||||
"noFieldsViewMode": "В этом рабочем процессе нет выбранных полей для отображения. Просмотрите полный рабочий процесс для настройки значений."
|
"noFieldsViewMode": "В этом рабочем процессе нет выбранных полей для отображения. Просмотрите полный рабочий процесс для настройки значений.",
|
||||||
|
"graph": "График",
|
||||||
|
"showEdgeLabels": "Показать метки на ребрах",
|
||||||
|
"showEdgeLabelsHelp": "Показать метки на ребрах, указывающие на соединенные узлы"
|
||||||
},
|
},
|
||||||
"controlnet": {
|
"controlnet": {
|
||||||
"amult": "a_mult",
|
"amult": "a_mult",
|
||||||
@ -917,8 +938,8 @@
|
|||||||
"lineartAnime": "Контурный рисунок в стиле аниме",
|
"lineartAnime": "Контурный рисунок в стиле аниме",
|
||||||
"mediapipeFaceDescription": "Обнаружение лиц с помощью Mediapipe",
|
"mediapipeFaceDescription": "Обнаружение лиц с помощью Mediapipe",
|
||||||
"hedDescription": "Целостное обнаружение границ",
|
"hedDescription": "Целостное обнаружение границ",
|
||||||
"setControlImageDimensions": "Установите размеры контрольного изображения на Ш/В",
|
"setControlImageDimensions": "Скопируйте размер в Ш/В (оптимизируйте для модели)",
|
||||||
"scribble": "каракули",
|
"scribble": "Штрихи",
|
||||||
"maxFaces": "Макс Лица",
|
"maxFaces": "Макс Лица",
|
||||||
"mlsdDescription": "Минималистичный детектор отрезков линии",
|
"mlsdDescription": "Минималистичный детектор отрезков линии",
|
||||||
"resizeSimple": "Изменить размер (простой)",
|
"resizeSimple": "Изменить размер (простой)",
|
||||||
@ -933,7 +954,18 @@
|
|||||||
"small": "Маленький",
|
"small": "Маленький",
|
||||||
"body": "Тело",
|
"body": "Тело",
|
||||||
"hands": "Руки",
|
"hands": "Руки",
|
||||||
"selectCLIPVisionModel": "Выбрать модель CLIP Vision"
|
"selectCLIPVisionModel": "Выбрать модель CLIP Vision",
|
||||||
|
"ipAdapterMethod": "Метод",
|
||||||
|
"full": "Всё",
|
||||||
|
"mlsd": "M-LSD",
|
||||||
|
"h": "H",
|
||||||
|
"style": "Только стиль",
|
||||||
|
"dwOpenpose": "DW Openpose",
|
||||||
|
"pidi": "PIDI",
|
||||||
|
"composition": "Только композиция",
|
||||||
|
"hed": "HED",
|
||||||
|
"beginEndStepPercentShort": "Начало/конец %",
|
||||||
|
"setControlImageDimensionsForce": "Скопируйте размер в Ш/В (игнорируйте модель)"
|
||||||
},
|
},
|
||||||
"boards": {
|
"boards": {
|
||||||
"autoAddBoard": "Авто добавление Доски",
|
"autoAddBoard": "Авто добавление Доски",
|
||||||
@ -1312,6 +1344,12 @@
|
|||||||
"paragraphs": [
|
"paragraphs": [
|
||||||
"Плавно укладывайте изображение вдоль вертикальной оси."
|
"Плавно укладывайте изображение вдоль вертикальной оси."
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"ipAdapterMethod": {
|
||||||
|
"heading": "Метод",
|
||||||
|
"paragraphs": [
|
||||||
|
"Метод, с помощью которого применяется текущий IP-адаптер."
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@ -1359,7 +1397,6 @@
|
|||||||
"completed": "Выполнено",
|
"completed": "Выполнено",
|
||||||
"queueBack": "Добавить в очередь",
|
"queueBack": "Добавить в очередь",
|
||||||
"cancelFailed": "Проблема с отменой элемента",
|
"cancelFailed": "Проблема с отменой элемента",
|
||||||
"queueCountPrediction": "{{promptsCount}} запросов × {{iterations}} изображений -> {{count}} генераций",
|
|
||||||
"batchQueued": "Пакетная очередь",
|
"batchQueued": "Пакетная очередь",
|
||||||
"pauseFailed": "Проблема с приостановкой рендеринга",
|
"pauseFailed": "Проблема с приостановкой рендеринга",
|
||||||
"clearFailed": "Проблема с очисткой очереди",
|
"clearFailed": "Проблема с очисткой очереди",
|
||||||
@ -1475,7 +1512,11 @@
|
|||||||
"projectWorkflows": "Рабочие процессы проекта",
|
"projectWorkflows": "Рабочие процессы проекта",
|
||||||
"defaultWorkflows": "Стандартные рабочие процессы",
|
"defaultWorkflows": "Стандартные рабочие процессы",
|
||||||
"name": "Имя",
|
"name": "Имя",
|
||||||
"noRecentWorkflows": "Нет последних рабочих процессов"
|
"noRecentWorkflows": "Нет последних рабочих процессов",
|
||||||
|
"loadWorkflow": "Рабочий процесс $t(common.load)",
|
||||||
|
"convertGraph": "Конвертировать график",
|
||||||
|
"loadFromGraph": "Загрузка рабочего процесса из графика",
|
||||||
|
"autoLayout": "Автоматическое расположение"
|
||||||
},
|
},
|
||||||
"hrf": {
|
"hrf": {
|
||||||
"enableHrf": "Включить исправление высокого разрешения",
|
"enableHrf": "Включить исправление высокого разрешения",
|
||||||
@ -1528,5 +1569,56 @@
|
|||||||
"addPromptTrigger": "Добавить триггер запроса",
|
"addPromptTrigger": "Добавить триггер запроса",
|
||||||
"compatibleEmbeddings": "Совместимые встраивания",
|
"compatibleEmbeddings": "Совместимые встраивания",
|
||||||
"noMatchingTriggers": "Нет соответствующих триггеров"
|
"noMatchingTriggers": "Нет соответствующих триггеров"
|
||||||
|
},
|
||||||
|
"controlLayers": {
|
||||||
|
"moveToBack": "На задний план",
|
||||||
|
"moveForward": "Переместить вперёд",
|
||||||
|
"moveBackward": "Переместить назад",
|
||||||
|
"brushSize": "Размер кисти",
|
||||||
|
"controlLayers": "Слои управления",
|
||||||
|
"globalMaskOpacity": "Глобальная непрозрачность маски",
|
||||||
|
"autoNegative": "Авто негатив",
|
||||||
|
"deletePrompt": "Удалить запрос",
|
||||||
|
"resetRegion": "Сбросить регион",
|
||||||
|
"debugLayers": "Слои отладки",
|
||||||
|
"rectangle": "Прямоугольник",
|
||||||
|
"maskPreviewColor": "Цвет предпросмотра маски",
|
||||||
|
"addNegativePrompt": "Добавить $t(common.negativePrompt)",
|
||||||
|
"regionalGuidance": "Региональная точность",
|
||||||
|
"opacity": "Непрозрачность",
|
||||||
|
"globalControlAdapter": "Глобальный $t(controlnet.controlAdapter_one)",
|
||||||
|
"globalControlAdapterLayer": "Глобальный $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
|
||||||
|
"globalIPAdapter": "Глобальный $t(common.ipAdapter)",
|
||||||
|
"globalIPAdapterLayer": "Глобальный $t(common.ipAdapter) $t(unifiedCanvas.layer)",
|
||||||
|
"opacityFilter": "Фильтр непрозрачности",
|
||||||
|
"deleteAll": "Удалить всё",
|
||||||
|
"addLayer": "Добавить слой",
|
||||||
|
"moveToFront": "На передний план",
|
||||||
|
"toggleVisibility": "Переключить видимость слоя",
|
||||||
|
"addPositivePrompt": "Добавить $t(common.positivePrompt)",
|
||||||
|
"addIPAdapter": "Добавить $t(common.ipAdapter)",
|
||||||
|
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
|
||||||
|
"resetProcessor": "Сброс процессора по умолчанию",
|
||||||
|
"clearProcessor": "Чистый процессор",
|
||||||
|
"globalInitialImage": "Глобальное исходное изображение",
|
||||||
|
"globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)",
|
||||||
|
"noLayersAdded": "Без слоев",
|
||||||
|
"layers_one": "Слой",
|
||||||
|
"layers_few": "Слоя",
|
||||||
|
"layers_many": "Слоев"
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"tabs": {
|
||||||
|
"generation": "Генерация",
|
||||||
|
"canvas": "Холст",
|
||||||
|
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
|
||||||
|
"models": "Модели",
|
||||||
|
"generationTab": "$t(ui.tabs.generation) $t(common.tab)",
|
||||||
|
"workflows": "Рабочие процессы",
|
||||||
|
"canvasTab": "$t(ui.tabs.canvas) $t(common.tab)",
|
||||||
|
"queueTab": "$t(ui.tabs.queue) $t(common.tab)",
|
||||||
|
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
||||||
|
"queue": "Очередь"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
"saveAs": "保存为",
|
"saveAs": "保存为",
|
||||||
"ai": "ai",
|
"ai": "ai",
|
||||||
"or": "或",
|
"or": "或",
|
||||||
"aboutDesc": "使用 Invoke 工作?查看:",
|
"aboutDesc": "使用 Invoke 工作?来看看:",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"loglevel": "日志级别",
|
"loglevel": "日志级别",
|
||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
@ -445,7 +445,6 @@
|
|||||||
"useX2Model": "图像太大,无法使用 x4 模型,使用 x2 模型作为替代",
|
"useX2Model": "图像太大,无法使用 x4 模型,使用 x2 模型作为替代",
|
||||||
"tooLarge": "图像太大无法进行放大,请选择更小的图像"
|
"tooLarge": "图像太大无法进行放大,请选择更小的图像"
|
||||||
},
|
},
|
||||||
"iterationsWithCount_other": "{{count}} 次迭代生成",
|
|
||||||
"cfgRescaleMultiplier": "CFG 重缩放倍数",
|
"cfgRescaleMultiplier": "CFG 重缩放倍数",
|
||||||
"useSize": "使用尺寸",
|
"useSize": "使用尺寸",
|
||||||
"setToOptimalSize": "优化模型大小",
|
"setToOptimalSize": "优化模型大小",
|
||||||
@ -853,7 +852,6 @@
|
|||||||
"pruneSucceeded": "从队列修剪 {{item_count}} 个已完成的项目",
|
"pruneSucceeded": "从队列修剪 {{item_count}} 个已完成的项目",
|
||||||
"notReady": "无法排队",
|
"notReady": "无法排队",
|
||||||
"batchFailedToQueue": "批次加入队列失败",
|
"batchFailedToQueue": "批次加入队列失败",
|
||||||
"queueCountPrediction": "{{promptsCount}} 提示词 × {{iterations}} 迭代次数 -> {{count}} 次生成",
|
|
||||||
"batchQueued": "加入队列的批次",
|
"batchQueued": "加入队列的批次",
|
||||||
"front": "前",
|
"front": "前",
|
||||||
"pruneTooltip": "修剪 {{item_count}} 个已完成的项目",
|
"pruneTooltip": "修剪 {{item_count}} 个已完成的项目",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
|
||||||
import openapiTS from 'openapi-typescript';
|
import openapiTS from 'openapi-typescript';
|
||||||
|
@ -67,6 +67,8 @@ export const useSocketIO = () => {
|
|||||||
|
|
||||||
if ($isDebugging.get() || import.meta.env.MODE === 'development') {
|
if ($isDebugging.get() || import.meta.env.MODE === 'development') {
|
||||||
window.$socketOptions = $socketOptions;
|
window.$socketOptions = $socketOptions;
|
||||||
|
// This is only enabled manually for debugging, console is allowed.
|
||||||
|
/* eslint-disable-next-line no-console */
|
||||||
console.log('Socket initialized', socket);
|
console.log('Socket initialized', socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +77,8 @@ export const useSocketIO = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
if ($isDebugging.get() || import.meta.env.MODE === 'development') {
|
if ($isDebugging.get() || import.meta.env.MODE === 'development') {
|
||||||
window.$socketOptions = undefined;
|
window.$socketOptions = undefined;
|
||||||
|
// This is only enabled manually for debugging, console is allowed.
|
||||||
|
/* eslint-disable-next-line no-console */
|
||||||
console.log('Socket teardown', socket);
|
console.log('Socket teardown', socket);
|
||||||
}
|
}
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
// This is only enabled manually for debugging, console is allowed.
|
||||||
|
|
||||||
import type { Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
|
import type { Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
|
||||||
import { diff } from 'jsondiffpatch';
|
import { diff } from 'jsondiffpatch';
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { UnknownAction } from '@reduxjs/toolkit';
|
import type { UnknownAction } from '@reduxjs/toolkit';
|
||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
|
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
|
||||||
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
||||||
import type { Graph } from 'services/api/types';
|
import type { Graph } from 'services/api/types';
|
||||||
import { socketGeneratorProgress } from 'services/events/actions';
|
import { socketGeneratorProgress } from 'services/events/actions';
|
||||||
@ -25,13 +24,6 @@ export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeTemplatesBuilt.match(action)) {
|
|
||||||
return {
|
|
||||||
...action,
|
|
||||||
payload: '<Node templates omitted>',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socketGeneratorProgress.match(action)) {
|
if (socketGeneratorProgress.match(action)) {
|
||||||
const sanitized = deepClone(action);
|
const sanitized = deepClone(action);
|
||||||
if (sanitized.payload.data.progress_image) {
|
if (sanitized.payload.data.progress_image) {
|
||||||
|
@ -21,7 +21,7 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
|
|||||||
|
|
||||||
const { canvas, nodes, controlAdapters, controlLayers } = getState();
|
const { canvas, nodes, controlAdapters, controlLayers } = getState();
|
||||||
deleted_images.forEach((image_name) => {
|
deleted_images.forEach((image_name) => {
|
||||||
const imageUsage = getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name);
|
const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, controlLayers.present, image_name);
|
||||||
|
|
||||||
if (imageUsage.isCanvasImage && !wasCanvasReset) {
|
if (imageUsage.isCanvasImage && !wasCanvasReset) {
|
||||||
dispatch(resetCanvas());
|
dispatch(resetCanvas());
|
||||||
|
@ -1,60 +1,54 @@
|
|||||||
import { isAnyOf } from '@reduxjs/toolkit';
|
import { isAnyOf } from '@reduxjs/toolkit';
|
||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import type { AppDispatch } from 'app/store/store';
|
||||||
import { parseify } from 'common/util/serialize';
|
import { parseify } from 'common/util/serialize';
|
||||||
import {
|
import {
|
||||||
caLayerImageChanged,
|
caLayerImageChanged,
|
||||||
caLayerIsProcessingImageChanged,
|
|
||||||
caLayerModelChanged,
|
caLayerModelChanged,
|
||||||
caLayerProcessedImageChanged,
|
caLayerProcessedImageChanged,
|
||||||
caLayerProcessorConfigChanged,
|
caLayerProcessorConfigChanged,
|
||||||
|
caLayerProcessorPendingBatchIdChanged,
|
||||||
|
caLayerRecalled,
|
||||||
isControlAdapterLayer,
|
isControlAdapterLayer,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
|
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
|
||||||
import { isImageOutput } from 'features/nodes/types/common';
|
import { isImageOutput } from 'features/nodes/types/common';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { isEqual } from 'lodash-es';
|
import { getImageDTO } from 'services/api/endpoints/images';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
|
||||||
import { queueApi } from 'services/api/endpoints/queue';
|
import { queueApi } from 'services/api/endpoints/queue';
|
||||||
import type { BatchConfig, ImageDTO } from 'services/api/types';
|
import type { BatchConfig } from 'services/api/types';
|
||||||
import { socketInvocationComplete } from 'services/events/actions';
|
import { socketInvocationComplete } from 'services/events/actions';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
const matcher = isAnyOf(caLayerImageChanged, caLayerProcessorConfigChanged, caLayerModelChanged);
|
const matcher = isAnyOf(caLayerImageChanged, caLayerProcessorConfigChanged, caLayerModelChanged, caLayerRecalled);
|
||||||
|
|
||||||
const DEBOUNCE_MS = 300;
|
const DEBOUNCE_MS = 300;
|
||||||
const log = logger('session');
|
const log = logger('session');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple helper to cancel a batch and reset the pending batch ID
|
||||||
|
*/
|
||||||
|
const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batchId: string) => {
|
||||||
|
const req = dispatch(queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: [batchId] }));
|
||||||
|
log.trace({ batchId }, 'Cancelling existing preprocessor batch');
|
||||||
|
try {
|
||||||
|
await req.unwrap();
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
} finally {
|
||||||
|
req.reset();
|
||||||
|
// Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist
|
||||||
|
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const addControlAdapterPreprocessor = (startAppListening: AppStartListening) => {
|
export const addControlAdapterPreprocessor = (startAppListening: AppStartListening) => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
matcher,
|
matcher,
|
||||||
effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take }) => {
|
effect: async (action, { dispatch, getState, cancelActiveListeners, delay, take, signal }) => {
|
||||||
const { layerId } = action.payload;
|
const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId;
|
||||||
const precheckLayerOriginal = getOriginalState()
|
|
||||||
.controlLayers.present.layers.filter(isControlAdapterLayer)
|
|
||||||
.find((l) => l.id === layerId);
|
|
||||||
const precheckLayer = getState()
|
|
||||||
.controlLayers.present.layers.filter(isControlAdapterLayer)
|
|
||||||
.find((l) => l.id === layerId);
|
|
||||||
|
|
||||||
// Conditions to bail
|
|
||||||
const layerDoesNotExist = !precheckLayer;
|
|
||||||
const layerHasNoImage = !precheckLayer?.controlAdapter.image;
|
|
||||||
const layerHasNoProcessorConfig = !precheckLayer?.controlAdapter.processorConfig;
|
|
||||||
const layerIsAlreadyProcessingImage = precheckLayer?.controlAdapter.isProcessingImage;
|
|
||||||
const areImageAndProcessorUnchanged =
|
|
||||||
isEqual(precheckLayer?.controlAdapter.image, precheckLayerOriginal?.controlAdapter.image) &&
|
|
||||||
isEqual(precheckLayer?.controlAdapter.processorConfig, precheckLayerOriginal?.controlAdapter.processorConfig);
|
|
||||||
|
|
||||||
if (
|
|
||||||
layerDoesNotExist ||
|
|
||||||
layerHasNoImage ||
|
|
||||||
layerHasNoProcessorConfig ||
|
|
||||||
areImageAndProcessorUnchanged ||
|
|
||||||
layerIsAlreadyProcessingImage
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel any in-progress instances of this listener
|
// Cancel any in-progress instances of this listener
|
||||||
cancelActiveListeners();
|
cancelActiveListeners();
|
||||||
@ -62,19 +56,31 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
|
|||||||
|
|
||||||
// Delay before starting actual work
|
// Delay before starting actual work
|
||||||
await delay(DEBOUNCE_MS);
|
await delay(DEBOUNCE_MS);
|
||||||
dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: true }));
|
|
||||||
|
|
||||||
// Double-check that we are still eligible for processing
|
// Double-check that we are still eligible for processing
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
|
const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
|
||||||
const image = layer?.controlAdapter.image;
|
|
||||||
const config = layer?.controlAdapter.processorConfig;
|
|
||||||
|
|
||||||
// If we have no image or there is no processor config, bail
|
// If we have no image or there is no processor config, bail
|
||||||
if (!layer || !image || !config) {
|
if (!layer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const image = layer.controlAdapter.image;
|
||||||
|
const config = layer.controlAdapter.processorConfig;
|
||||||
|
|
||||||
|
if (!image || !config) {
|
||||||
|
// The user has reset the image or config, so we should clear the processed image
|
||||||
|
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, the user has stopped fiddling with the processor settings and there is a processor selected.
|
||||||
|
|
||||||
|
// If there is a pending processor batch, cancel it.
|
||||||
|
if (layer.controlAdapter.processorPendingBatchId) {
|
||||||
|
cancelProcessorBatch(dispatch, layerId, layer.controlAdapter.processorPendingBatchId);
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-expect-error: TS isn't able to narrow the typing of buildNode and `config` will error...
|
// @ts-expect-error: TS isn't able to narrow the typing of buildNode and `config` will error...
|
||||||
const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config);
|
const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config);
|
||||||
const enqueueBatchArg: BatchConfig = {
|
const enqueueBatchArg: BatchConfig = {
|
||||||
@ -82,7 +88,11 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
|
|||||||
batch: {
|
batch: {
|
||||||
graph: {
|
graph: {
|
||||||
nodes: {
|
nodes: {
|
||||||
[processorNode.id]: { ...processorNode, is_intermediate: true },
|
[processorNode.id]: {
|
||||||
|
...processorNode,
|
||||||
|
// Control images are always intermediate - do not save to gallery
|
||||||
|
is_intermediate: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
edges: [],
|
edges: [],
|
||||||
},
|
},
|
||||||
@ -90,16 +100,21 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kick off the processor batch
|
||||||
|
const req = dispatch(
|
||||||
|
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
|
||||||
|
fixedCacheKey: 'enqueueBatch',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const req = dispatch(
|
|
||||||
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
|
|
||||||
fixedCacheKey: 'enqueueBatch',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const enqueueResult = await req.unwrap();
|
const enqueueResult = await req.unwrap();
|
||||||
req.reset();
|
// TODO(psyche): Update the pydantic models, pretty sure we will _always_ have a batch_id here, but the model says it's optional
|
||||||
|
assert(enqueueResult.batch.batch_id, 'Batch ID not returned from queue');
|
||||||
|
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id }));
|
||||||
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
|
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
|
||||||
|
|
||||||
|
// Wait for the processor node to complete
|
||||||
const [invocationCompleteAction] = await take(
|
const [invocationCompleteAction] = await take(
|
||||||
(action): action is ReturnType<typeof socketInvocationComplete> =>
|
(action): action is ReturnType<typeof socketInvocationComplete> =>
|
||||||
socketInvocationComplete.match(action) &&
|
socketInvocationComplete.match(action) &&
|
||||||
@ -108,48 +123,51 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
|
|||||||
);
|
);
|
||||||
|
|
||||||
// We still have to check the output type
|
// We still have to check the output type
|
||||||
if (isImageOutput(invocationCompleteAction.payload.data.result)) {
|
assert(
|
||||||
const { image_name } = invocationCompleteAction.payload.data.result.image;
|
isImageOutput(invocationCompleteAction.payload.data.result),
|
||||||
|
`Processor did not return an image output, got: ${invocationCompleteAction.payload.data.result}`
|
||||||
|
);
|
||||||
|
const { image_name } = invocationCompleteAction.payload.data.result.image;
|
||||||
|
|
||||||
// Wait for the ImageDTO to be received
|
const imageDTO = await getImageDTO(image_name);
|
||||||
const [{ payload }] = await take(
|
assert(imageDTO, "Failed to fetch processor output's image DTO");
|
||||||
(action) =>
|
|
||||||
imagesApi.endpoints.getImageDTO.matchFulfilled(action) && action.payload.image_name === image_name
|
|
||||||
);
|
|
||||||
|
|
||||||
const imageDTO = payload as ImageDTO;
|
// Whew! We made it. Update the layer with the processed image
|
||||||
|
log.debug({ layerId, imageDTO }, 'ControlNet image processed');
|
||||||
log.debug({ layerId, imageDTO }, 'ControlNet image processed');
|
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO }));
|
||||||
|
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
|
||||||
// Update the processed image in the store
|
|
||||||
dispatch(
|
|
||||||
caLayerProcessedImageChanged({
|
|
||||||
layerId,
|
|
||||||
imageDTO,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false }));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
if (signal.aborted) {
|
||||||
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
|
// The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now).
|
||||||
dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false }));
|
const pendingBatchId = getState()
|
||||||
|
.controlLayers.present.layers.filter(isControlAdapterLayer)
|
||||||
|
.find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId;
|
||||||
|
if (pendingBatchId) {
|
||||||
|
cancelProcessorBatch(dispatch, layerId, pendingBatchId);
|
||||||
|
}
|
||||||
|
log.trace('Control Adapter preprocessor cancelled');
|
||||||
|
} else {
|
||||||
|
// Some other error condition...
|
||||||
|
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
|
||||||
|
|
||||||
if (error instanceof Object) {
|
if (error instanceof Object) {
|
||||||
if ('data' in error && 'status' in error) {
|
if ('data' in error && 'status' in error) {
|
||||||
if (error.status === 403) {
|
if (error.status === 403) {
|
||||||
dispatch(caLayerImageChanged({ layerId, imageDTO: null }));
|
dispatch(caLayerImageChanged({ layerId, imageDTO: null }));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addToast({
|
addToast({
|
||||||
title: t('queue.graphFailedToQueue'),
|
title: t('queue.graphFailedToQueue'),
|
||||||
status: 'error',
|
status: 'error',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
req.reset();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -8,8 +8,8 @@ import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
|||||||
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
||||||
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
||||||
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
||||||
import { buildCanvasGraph } from 'features/nodes/util/graph/buildCanvasGraph';
|
|
||||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||||
|
import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { queueApi } from 'services/api/endpoints/queue';
|
import { queueApi } from 'services/api/endpoints/queue';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { enqueueRequested } from 'app/store/actions';
|
import { enqueueRequested } from 'app/store/actions';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph';
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph';
|
|
||||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||||
|
import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph';
|
||||||
|
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph';
|
||||||
import { queueApi } from 'services/api/endpoints/queue';
|
import { queueApi } from 'services/api/endpoints/queue';
|
||||||
|
|
||||||
export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => {
|
export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => {
|
||||||
@ -11,12 +12,13 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
enqueueRequested.match(action) && action.payload.tabName === 'generation',
|
enqueueRequested.match(action) && action.payload.tabName === 'generation',
|
||||||
effect: async (action, { getState, dispatch }) => {
|
effect: async (action, { getState, dispatch }) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
const { shouldShowProgressInViewer } = state.ui;
|
||||||
const model = state.generation.model;
|
const model = state.generation.model;
|
||||||
const { prepend } = action.payload;
|
const { prepend } = action.payload;
|
||||||
|
|
||||||
let graph;
|
let graph;
|
||||||
|
|
||||||
if (model && model.base === 'sdxl') {
|
if (model?.base === 'sdxl') {
|
||||||
graph = await buildGenerationTabSDXLGraph(state);
|
graph = await buildGenerationTabSDXLGraph(state);
|
||||||
} else {
|
} else {
|
||||||
graph = await buildGenerationTabGraph(state);
|
graph = await buildGenerationTabGraph(state);
|
||||||
@ -29,7 +31,14 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
fixedCacheKey: 'enqueueBatch',
|
fixedCacheKey: 'enqueueBatch',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
req.reset();
|
try {
|
||||||
|
await req.unwrap();
|
||||||
|
if (shouldShowProgressInViewer) {
|
||||||
|
dispatch(isImageViewerOpenChanged(true));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
req.reset();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -11,9 +11,9 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
|
|||||||
enqueueRequested.match(action) && action.payload.tabName === 'workflows',
|
enqueueRequested.match(action) && action.payload.tabName === 'workflows',
|
||||||
effect: async (action, { getState, dispatch }) => {
|
effect: async (action, { getState, dispatch }) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { nodes, edges } = state.nodes;
|
const { nodes, edges } = state.nodes.present;
|
||||||
const workflow = state.workflow;
|
const workflow = state.workflow;
|
||||||
const graph = buildNodesGraph(state.nodes);
|
const graph = buildNodesGraph(state.nodes.present);
|
||||||
const builtWorkflow = buildWorkflowWithValidation({
|
const builtWorkflow = buildWorkflowWithValidation({
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
@ -39,7 +39,11 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
|
|||||||
fixedCacheKey: 'enqueueBatch',
|
fixedCacheKey: 'enqueueBatch',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
req.reset();
|
try {
|
||||||
|
await req.unwrap();
|
||||||
|
} finally {
|
||||||
|
req.reset();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import { isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { imagesSelectors } from 'services/api/util';
|
import { imagesSelectors } from 'services/api/util';
|
||||||
@ -62,7 +62,6 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
|
|||||||
} else {
|
} else {
|
||||||
dispatch(selectionChanged([imageDTO]));
|
dispatch(selectionChanged([imageDTO]));
|
||||||
}
|
}
|
||||||
dispatch(isImageViewerOpenChanged(true));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { parseify } from 'common/util/serialize';
|
import { parseify } from 'common/util/serialize';
|
||||||
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
|
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||||
import { parseSchema } from 'features/nodes/util/schema/parseSchema';
|
import { parseSchema } from 'features/nodes/util/schema/parseSchema';
|
||||||
import { size } from 'lodash-es';
|
import { size } from 'lodash-es';
|
||||||
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
||||||
@ -9,7 +9,7 @@ import { appInfoApi } from 'services/api/endpoints/appInfo';
|
|||||||
export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening) => {
|
export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening) => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
matcher: appInfoApi.endpoints.getOpenAPISchema.matchFulfilled,
|
matcher: appInfoApi.endpoints.getOpenAPISchema.matchFulfilled,
|
||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { getState }) => {
|
||||||
const log = logger('system');
|
const log = logger('system');
|
||||||
const schemaJSON = action.payload;
|
const schemaJSON = action.payload;
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening
|
|||||||
|
|
||||||
log.debug({ nodeTemplates: parseify(nodeTemplates) }, `Built ${size(nodeTemplates)} node templates`);
|
log.debug({ nodeTemplates: parseify(nodeTemplates) }, `Built ${size(nodeTemplates)} node templates`);
|
||||||
|
|
||||||
dispatch(nodeTemplatesBuilt(nodeTemplates));
|
$templates.set(nodeTemplates);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ import type { ImageDTO } from 'services/api/types';
|
|||||||
import { imagesSelectors } from 'services/api/util';
|
import { imagesSelectors } from 'services/api/util';
|
||||||
|
|
||||||
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||||
state.nodes.nodes.forEach((node) => {
|
state.nodes.present.nodes.forEach((node) => {
|
||||||
if (!isInvocationNode(node)) {
|
if (!isInvocationNode(node)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -73,25 +73,25 @@ const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, ima
|
|||||||
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||||
state.controlLayers.present.layers.forEach((l) => {
|
state.controlLayers.present.layers.forEach((l) => {
|
||||||
if (isRegionalGuidanceLayer(l)) {
|
if (isRegionalGuidanceLayer(l)) {
|
||||||
if (l.ipAdapters.some((ipa) => ipa.image?.imageName === imageDTO.image_name)) {
|
if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) {
|
||||||
dispatch(layerDeleted(l.id));
|
dispatch(layerDeleted(l.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isControlAdapterLayer(l)) {
|
if (isControlAdapterLayer(l)) {
|
||||||
if (
|
if (
|
||||||
l.controlAdapter.image?.imageName === imageDTO.image_name ||
|
l.controlAdapter.image?.name === imageDTO.image_name ||
|
||||||
l.controlAdapter.processedImage?.imageName === imageDTO.image_name
|
l.controlAdapter.processedImage?.name === imageDTO.image_name
|
||||||
) {
|
) {
|
||||||
dispatch(layerDeleted(l.id));
|
dispatch(layerDeleted(l.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isIPAdapterLayer(l)) {
|
if (isIPAdapterLayer(l)) {
|
||||||
if (l.ipAdapter.image?.imageName === imageDTO.image_name) {
|
if (l.ipAdapter.image?.name === imageDTO.image_name) {
|
||||||
dispatch(layerDeleted(l.id));
|
dispatch(layerDeleted(l.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isInitialImageLayer(l)) {
|
if (isInitialImageLayer(l)) {
|
||||||
if (l.image?.imageName === imageDTO.image_name) {
|
if (l.image?.name === imageDTO.image_name) {
|
||||||
dispatch(layerDeleted(l.id));
|
dispatch(layerDeleted(l.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
|
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
|
||||||
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
import { socketGeneratorProgress } from 'services/events/actions';
|
import { socketGeneratorProgress } from 'services/events/actions';
|
||||||
|
|
||||||
const log = logger('socketio');
|
const log = logger('socketio');
|
||||||
@ -9,6 +12,13 @@ export const addGeneratorProgressEventListener = (startAppListening: AppStartLis
|
|||||||
actionCreator: socketGeneratorProgress,
|
actionCreator: socketGeneratorProgress,
|
||||||
effect: (action) => {
|
effect: (action) => {
|
||||||
log.trace(action.payload, `Generator progress`);
|
log.trace(action.payload, `Generator progress`);
|
||||||
|
const { source_node_id, step, total_steps, progress_image } = action.payload.data;
|
||||||
|
const nes = deepClone($nodeExecutionStates.get()[source_node_id]);
|
||||||
|
if (nes) {
|
||||||
|
nes.status = zNodeStatus.enum.IN_PROGRESS;
|
||||||
|
nes.progress = (step + 1) / total_steps;
|
||||||
|
nes.progressImage = progress_image ?? null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { parseify } from 'common/util/serialize';
|
import { parseify } from 'common/util/serialize';
|
||||||
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
||||||
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
|
import {
|
||||||
|
boardIdSelected,
|
||||||
|
galleryViewChanged,
|
||||||
|
imageSelected,
|
||||||
|
isImageViewerOpenChanged,
|
||||||
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||||
|
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
import { isImageOutput } from 'features/nodes/types/common';
|
import { isImageOutput } from 'features/nodes/types/common';
|
||||||
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants';
|
import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
@ -23,7 +31,7 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
|
|||||||
const { data } = action.payload;
|
const { data } = action.payload;
|
||||||
log.debug({ data: parseify(data) }, `Invocation complete (${action.payload.data.node.type})`);
|
log.debug({ data: parseify(data) }, `Invocation complete (${action.payload.data.node.type})`);
|
||||||
|
|
||||||
const { result, node, queue_batch_id } = data;
|
const { result, node, queue_batch_id, source_node_id } = data;
|
||||||
// This complete event has an associated image output
|
// This complete event has an associated image output
|
||||||
if (isImageOutput(result) && !nodeTypeDenylist.includes(node.type)) {
|
if (isImageOutput(result) && !nodeTypeDenylist.includes(node.type)) {
|
||||||
const { image_name } = result.image;
|
const { image_name } = result.image;
|
||||||
@ -101,9 +109,20 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch(imageSelected(imageDTO));
|
dispatch(imageSelected(imageDTO));
|
||||||
|
dispatch(isImageViewerOpenChanged(true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nes = deepClone($nodeExecutionStates.get()[source_node_id]);
|
||||||
|
if (nes) {
|
||||||
|
nes.status = zNodeStatus.enum.COMPLETED;
|
||||||
|
if (nes.progress !== null) {
|
||||||
|
nes.progress = 1;
|
||||||
|
}
|
||||||
|
nes.outputs.push(result);
|
||||||
|
upsertExecutionState(nes.nodeId, nes);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
|
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
import { socketInvocationError } from 'services/events/actions';
|
import { socketInvocationError } from 'services/events/actions';
|
||||||
|
|
||||||
const log = logger('socketio');
|
const log = logger('socketio');
|
||||||
@ -9,6 +12,15 @@ export const addInvocationErrorEventListener = (startAppListening: AppStartListe
|
|||||||
actionCreator: socketInvocationError,
|
actionCreator: socketInvocationError,
|
||||||
effect: (action) => {
|
effect: (action) => {
|
||||||
log.error(action.payload, `Invocation error (${action.payload.data.node.type})`);
|
log.error(action.payload, `Invocation error (${action.payload.data.node.type})`);
|
||||||
|
const { source_node_id } = action.payload.data;
|
||||||
|
const nes = deepClone($nodeExecutionStates.get()[source_node_id]);
|
||||||
|
if (nes) {
|
||||||
|
nes.status = zNodeStatus.enum.FAILED;
|
||||||
|
nes.error = action.payload.data.error;
|
||||||
|
nes.progress = null;
|
||||||
|
nes.progressImage = null;
|
||||||
|
upsertExecutionState(nes.nodeId, nes);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
|
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
import { socketInvocationStarted } from 'services/events/actions';
|
import { socketInvocationStarted } from 'services/events/actions';
|
||||||
|
|
||||||
const log = logger('socketio');
|
const log = logger('socketio');
|
||||||
@ -9,6 +12,12 @@ export const addInvocationStartedEventListener = (startAppListening: AppStartLis
|
|||||||
actionCreator: socketInvocationStarted,
|
actionCreator: socketInvocationStarted,
|
||||||
effect: (action) => {
|
effect: (action) => {
|
||||||
log.debug(action.payload, `Invocation started (${action.payload.data.node.type})`);
|
log.debug(action.payload, `Invocation started (${action.payload.data.node.type})`);
|
||||||
|
const { source_node_id } = action.payload.data;
|
||||||
|
const nes = deepClone($nodeExecutionStates.get()[source_node_id]);
|
||||||
|
if (nes) {
|
||||||
|
nes.status = zNodeStatus.enum.IN_PROGRESS;
|
||||||
|
upsertExecutionState(nes.nodeId, nes);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
|
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
|
||||||
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
|
import { forEach } from 'lodash-es';
|
||||||
import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
|
import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
|
||||||
import { socketQueueItemStatusChanged } from 'services/events/actions';
|
import { socketQueueItemStatusChanged } from 'services/events/actions';
|
||||||
|
|
||||||
@ -54,6 +58,21 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening:
|
|||||||
dispatch(
|
dispatch(
|
||||||
queueApi.util.invalidateTags(['CurrentSessionQueueItem', 'NextSessionQueueItem', 'InvocationCacheStatus'])
|
queueApi.util.invalidateTags(['CurrentSessionQueueItem', 'NextSessionQueueItem', 'InvocationCacheStatus'])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (['in_progress'].includes(action.payload.data.queue_item.status)) {
|
||||||
|
forEach($nodeExecutionStates.get(), (nes) => {
|
||||||
|
if (!nes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clone = deepClone(nes);
|
||||||
|
clone.status = zNodeStatus.enum.PENDING;
|
||||||
|
clone.error = null;
|
||||||
|
clone.progress = null;
|
||||||
|
clone.progressImage = null;
|
||||||
|
clone.outputs = [];
|
||||||
|
$nodeExecutionStates.setKey(clone.nodeId, clone);
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
||||||
import { nodeReplaced } from 'features/nodes/store/nodesSlice';
|
import { $templates, nodeReplaced } from 'features/nodes/store/nodesSlice';
|
||||||
import { NodeUpdateError } from 'features/nodes/types/error';
|
import { NodeUpdateError } from 'features/nodes/types/error';
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate';
|
import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate';
|
||||||
@ -14,7 +14,8 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi
|
|||||||
actionCreator: updateAllNodesRequested,
|
actionCreator: updateAllNodesRequested,
|
||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { dispatch, getState }) => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const { nodes, templates } = getState().nodes;
|
const { nodes } = getState().nodes.present;
|
||||||
|
const templates = $templates.get();
|
||||||
|
|
||||||
let unableToUpdateCount = 0;
|
let unableToUpdateCount = 0;
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi
|
|||||||
unableToUpdateCount++;
|
unableToUpdateCount++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!getNeedsUpdate(node, template)) {
|
if (!getNeedsUpdate(node.data, template)) {
|
||||||
// No need to increment the count here, since we're not actually updating
|
// No need to increment the count here, since we're not actually updating
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2,32 +2,51 @@ import { logger } from 'app/logging/logger';
|
|||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { parseify } from 'common/util/serialize';
|
import { parseify } from 'common/util/serialize';
|
||||||
import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions';
|
import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions';
|
||||||
|
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||||
import { $flow } from 'features/nodes/store/reactFlowInstance';
|
import { $flow } from 'features/nodes/store/reactFlowInstance';
|
||||||
|
import type { Templates } from 'features/nodes/store/types';
|
||||||
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
|
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
|
||||||
|
import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow';
|
||||||
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
|
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { makeToast } from 'features/system/util/makeToast';
|
import { makeToast } from 'features/system/util/makeToast';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
import type { GraphAndWorkflowResponse, NonNullableGraph } from 'services/api/types';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { fromZodError } from 'zod-validation-error';
|
import { fromZodError } from 'zod-validation-error';
|
||||||
|
|
||||||
|
const getWorkflow = (data: GraphAndWorkflowResponse, templates: Templates) => {
|
||||||
|
if (data.workflow) {
|
||||||
|
// Prefer to load the workflow if it's available - it has more information
|
||||||
|
const parsed = JSON.parse(data.workflow);
|
||||||
|
return validateWorkflow(parsed, templates);
|
||||||
|
} else if (data.graph) {
|
||||||
|
// Else we fall back on the graph, using the graphToWorkflow function to convert and do layout
|
||||||
|
const parsed = JSON.parse(data.graph);
|
||||||
|
const workflow = graphToWorkflow(parsed as NonNullableGraph, true);
|
||||||
|
return validateWorkflow(workflow, templates);
|
||||||
|
} else {
|
||||||
|
throw new Error('No workflow or graph provided');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const addWorkflowLoadRequestedListener = (startAppListening: AppStartListening) => {
|
export const addWorkflowLoadRequestedListener = (startAppListening: AppStartListening) => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: workflowLoadRequested,
|
actionCreator: workflowLoadRequested,
|
||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { dispatch }) => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const { workflow, asCopy } = action.payload;
|
const { data, asCopy } = action.payload;
|
||||||
const nodeTemplates = getState().nodes.templates;
|
const nodeTemplates = $templates.get();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { workflow: validatedWorkflow, warnings } = validateWorkflow(workflow, nodeTemplates);
|
const { workflow, warnings } = getWorkflow(data, nodeTemplates);
|
||||||
|
|
||||||
if (asCopy) {
|
if (asCopy) {
|
||||||
// If we're loading a copy, we need to remove the ID so that the backend will create a new workflow
|
// If we're loading a copy, we need to remove the ID so that the backend will create a new workflow
|
||||||
delete validatedWorkflow.id;
|
delete workflow.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(workflowLoaded(validatedWorkflow));
|
dispatch(workflowLoaded(workflow));
|
||||||
if (!warnings.length) {
|
if (!warnings.length) {
|
||||||
dispatch(
|
dispatch(
|
||||||
addToast(
|
addToast(
|
||||||
|
@ -21,7 +21,8 @@ import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/galle
|
|||||||
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
|
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
|
||||||
import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice';
|
import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice';
|
||||||
import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||||
import { nodesPersistConfig, nodesSlice } from 'features/nodes/store/nodesSlice';
|
import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
|
||||||
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
|
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
|
||||||
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
|
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
|
||||||
import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice';
|
import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice';
|
||||||
@ -50,7 +51,7 @@ const allReducers = {
|
|||||||
[canvasSlice.name]: canvasSlice.reducer,
|
[canvasSlice.name]: canvasSlice.reducer,
|
||||||
[gallerySlice.name]: gallerySlice.reducer,
|
[gallerySlice.name]: gallerySlice.reducer,
|
||||||
[generationSlice.name]: generationSlice.reducer,
|
[generationSlice.name]: generationSlice.reducer,
|
||||||
[nodesSlice.name]: nodesSlice.reducer,
|
[nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig),
|
||||||
[postprocessingSlice.name]: postprocessingSlice.reducer,
|
[postprocessingSlice.name]: postprocessingSlice.reducer,
|
||||||
[systemSlice.name]: systemSlice.reducer,
|
[systemSlice.name]: systemSlice.reducer,
|
||||||
[configSlice.name]: configSlice.reducer,
|
[configSlice.name]: configSlice.reducer,
|
||||||
@ -66,6 +67,7 @@ const allReducers = {
|
|||||||
[workflowSlice.name]: workflowSlice.reducer,
|
[workflowSlice.name]: workflowSlice.reducer,
|
||||||
[hrfSlice.name]: hrfSlice.reducer,
|
[hrfSlice.name]: hrfSlice.reducer,
|
||||||
[controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig),
|
[controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig),
|
||||||
|
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
|
||||||
[api.reducerPath]: api.reducer,
|
[api.reducerPath]: api.reducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -111,6 +113,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
|||||||
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
|
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
|
||||||
[hrfPersistConfig.name]: hrfPersistConfig,
|
[hrfPersistConfig.name]: hrfPersistConfig,
|
||||||
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
|
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
|
||||||
|
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
const unserialize: UnserializeFunction = (data, key) => {
|
const unserialize: UnserializeFunction = (data, key) => {
|
||||||
|
@ -70,6 +70,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
onMouseOver,
|
onMouseOver,
|
||||||
onMouseOut,
|
onMouseOut,
|
||||||
dataTestId,
|
dataTestId,
|
||||||
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
@ -138,6 +139,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
minH={minSize ? minSize : undefined}
|
minH={minSize ? minSize : undefined}
|
||||||
userSelect="none"
|
userSelect="none"
|
||||||
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
|
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{imageDTO && (
|
{imageDTO && (
|
||||||
<Flex
|
<Flex
|
||||||
|
@ -13,6 +13,7 @@ type UseGroupedModelComboboxArg<T extends AnyModelConfig> = {
|
|||||||
onChange: (value: T | null) => void;
|
onChange: (value: T | null) => void;
|
||||||
getIsDisabled?: (model: T) => boolean;
|
getIsDisabled?: (model: T) => boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
groupByType?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseGroupedModelComboboxReturn = {
|
type UseGroupedModelComboboxReturn = {
|
||||||
@ -23,17 +24,21 @@ type UseGroupedModelComboboxReturn = {
|
|||||||
noOptionsMessage: () => string;
|
noOptionsMessage: () => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const groupByBaseFunc = <T extends AnyModelConfig>(model: T) => model.base.toUpperCase();
|
||||||
|
const groupByBaseAndTypeFunc = <T extends AnyModelConfig>(model: T) =>
|
||||||
|
`${model.base.toUpperCase()} / ${model.type.replaceAll('_', ' ').toUpperCase()}`;
|
||||||
|
|
||||||
export const useGroupedModelCombobox = <T extends AnyModelConfig>(
|
export const useGroupedModelCombobox = <T extends AnyModelConfig>(
|
||||||
arg: UseGroupedModelComboboxArg<T>
|
arg: UseGroupedModelComboboxArg<T>
|
||||||
): UseGroupedModelComboboxReturn => {
|
): UseGroupedModelComboboxReturn => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const base_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl');
|
const base_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl');
|
||||||
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading } = arg;
|
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg;
|
||||||
const options = useMemo<GroupBase<ComboboxOption>[]>(() => {
|
const options = useMemo<GroupBase<ComboboxOption>[]>(() => {
|
||||||
if (!modelConfigs) {
|
if (!modelConfigs) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const groupedModels = groupBy(modelConfigs, 'base');
|
const groupedModels = groupBy(modelConfigs, groupByType ? groupByBaseAndTypeFunc : groupByBaseFunc);
|
||||||
const _options = reduce(
|
const _options = reduce(
|
||||||
groupedModels,
|
groupedModels,
|
||||||
(acc, val, label) => {
|
(acc, val, label) => {
|
||||||
@ -49,9 +54,9 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
|
|||||||
},
|
},
|
||||||
[] as GroupBase<ComboboxOption>[]
|
[] as GroupBase<ComboboxOption>[]
|
||||||
);
|
);
|
||||||
_options.sort((a) => (a.label === base_model ? -1 : 1));
|
_options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base_model) ? -1 : 1));
|
||||||
return _options;
|
return _options;
|
||||||
}, [getIsDisabled, modelConfigs, base_model]);
|
}, [modelConfigs, groupByType, getIsDisabled, base_model]);
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import {
|
||||||
@ -6,187 +7,230 @@ import {
|
|||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||||
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import type { Layer } from 'features/controlLayers/store/types';
|
||||||
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
|
import type { Templates } from 'features/nodes/store/types';
|
||||||
|
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
|
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
|
||||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { forEach } from 'lodash-es';
|
import { forEach, upperFirst } from 'lodash-es';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { getConnectedEdges } from 'reactflow';
|
import { getConnectedEdges } from 'reactflow';
|
||||||
|
|
||||||
const selector = createMemoizedSelector(
|
const LAYER_TYPE_TO_TKEY: Record<Layer['type'], string> = {
|
||||||
[
|
initial_image_layer: 'controlLayers.globalInitialImage',
|
||||||
selectControlAdaptersSlice,
|
control_adapter_layer: 'controlLayers.globalControlAdapter',
|
||||||
selectGenerationSlice,
|
ip_adapter_layer: 'controlLayers.globalIPAdapter',
|
||||||
selectSystemSlice,
|
regional_guidance_layer: 'controlLayers.regionalGuidance',
|
||||||
selectNodesSlice,
|
};
|
||||||
selectDynamicPromptsSlice,
|
|
||||||
selectControlLayersSlice,
|
|
||||||
activeTabNameSelector,
|
|
||||||
],
|
|
||||||
(controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => {
|
|
||||||
const { model } = generation;
|
|
||||||
const { positivePrompt } = controlLayers.present;
|
|
||||||
|
|
||||||
const { isConnected } = system;
|
const createSelector = (templates: Templates) =>
|
||||||
|
createMemoizedSelector(
|
||||||
|
[
|
||||||
|
selectControlAdaptersSlice,
|
||||||
|
selectGenerationSlice,
|
||||||
|
selectSystemSlice,
|
||||||
|
selectNodesSlice,
|
||||||
|
selectWorkflowSettingsSlice,
|
||||||
|
selectDynamicPromptsSlice,
|
||||||
|
selectControlLayersSlice,
|
||||||
|
activeTabNameSelector,
|
||||||
|
],
|
||||||
|
(controlAdapters, generation, system, nodes, workflowSettings, dynamicPrompts, controlLayers, activeTabName) => {
|
||||||
|
const { model } = generation;
|
||||||
|
const { size } = controlLayers.present;
|
||||||
|
const { positivePrompt } = controlLayers.present;
|
||||||
|
|
||||||
const reasons: string[] = [];
|
const { isConnected } = system;
|
||||||
|
|
||||||
// Cannot generate if not connected
|
const reasons: { prefix?: string; content: string }[] = [];
|
||||||
if (!isConnected) {
|
|
||||||
reasons.push(i18n.t('parameters.invoke.systemDisconnected'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTabName === 'workflows') {
|
// Cannot generate if not connected
|
||||||
if (nodes.shouldValidateGraph) {
|
if (!isConnected) {
|
||||||
if (!nodes.nodes.length) {
|
reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') });
|
||||||
reasons.push(i18n.t('parameters.invoke.noNodesInGraph'));
|
}
|
||||||
|
|
||||||
|
if (activeTabName === 'workflows') {
|
||||||
|
if (workflowSettings.shouldValidateGraph) {
|
||||||
|
if (!nodes.nodes.length) {
|
||||||
|
reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') });
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.nodes.forEach((node) => {
|
||||||
|
if (!isInvocationNode(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeTemplate = templates[node.data.type];
|
||||||
|
|
||||||
|
if (!nodeTemplate) {
|
||||||
|
// Node type not found
|
||||||
|
reasons.push({ content: i18n.t('parameters.invoke.missingNodeTemplate') });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectedEdges = getConnectedEdges([node], nodes.edges);
|
||||||
|
|
||||||
|
forEach(node.data.inputs, (field) => {
|
||||||
|
const fieldTemplate = nodeTemplate.inputs[field.name];
|
||||||
|
const hasConnection = connectedEdges.some(
|
||||||
|
(edge) => edge.target === node.id && edge.targetHandle === field.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fieldTemplate) {
|
||||||
|
reasons.push({ content: i18n.t('parameters.invoke.missingFieldTemplate') });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldTemplate.required && field.value === undefined && !hasConnection) {
|
||||||
|
reasons.push({
|
||||||
|
content: i18n.t('parameters.invoke.missingInputForField', {
|
||||||
|
nodeLabel: node.data.label || nodeTemplate.title,
|
||||||
|
fieldLabel: field.label || fieldTemplate.title,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
|
||||||
|
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.nodes.forEach((node) => {
|
if (!model) {
|
||||||
if (!isInvocationNode(node)) {
|
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const nodeTemplate = nodes.templates[node.data.type];
|
if (activeTabName === 'generation') {
|
||||||
|
// Handling for generation tab
|
||||||
|
controlLayers.present.layers
|
||||||
|
.filter((l) => l.isEnabled)
|
||||||
|
.forEach((l, i) => {
|
||||||
|
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||||
|
const layerNumber = i + 1;
|
||||||
|
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]);
|
||||||
|
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||||
|
const problems: string[] = [];
|
||||||
|
if (l.type === 'control_adapter_layer') {
|
||||||
|
// Must have model
|
||||||
|
if (!l.controlAdapter.model) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
|
||||||
|
}
|
||||||
|
// Model base must match
|
||||||
|
if (l.controlAdapter.model?.base !== model?.base) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
|
||||||
|
}
|
||||||
|
// Must have a control image OR, if it has a processor, it must have a processed image
|
||||||
|
if (!l.controlAdapter.image) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected'));
|
||||||
|
} else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed'));
|
||||||
|
}
|
||||||
|
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
|
||||||
|
if (l.controlAdapter.type === 't2i_adapter') {
|
||||||
|
const multiple = model?.base === 'sdxl' ? 32 : 64;
|
||||||
|
if (size.width % multiple !== 0 || size.height % multiple !== 0) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!nodeTemplate) {
|
if (l.type === 'ip_adapter_layer') {
|
||||||
// Node type not found
|
// Must have model
|
||||||
reasons.push(i18n.t('parameters.invoke.missingNodeTemplate'));
|
if (!l.ipAdapter.model) {
|
||||||
return;
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
||||||
}
|
}
|
||||||
|
// Model base must match
|
||||||
|
if (l.ipAdapter.model?.base !== model?.base) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
|
||||||
|
}
|
||||||
|
// Must have an image
|
||||||
|
if (!l.ipAdapter.image) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const connectedEdges = getConnectedEdges([node], nodes.edges);
|
if (l.type === 'initial_image_layer') {
|
||||||
|
// Must have an image
|
||||||
|
if (!l.image) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
forEach(node.data.inputs, (field) => {
|
if (l.type === 'regional_guidance_layer') {
|
||||||
const fieldTemplate = nodeTemplate.inputs[field.name];
|
// Must have a region
|
||||||
const hasConnection = connectedEdges.some(
|
if (l.maskObjects.length === 0) {
|
||||||
(edge) => edge.target === node.id && edge.targetHandle === field.name
|
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
|
||||||
);
|
}
|
||||||
|
// Must have at least 1 prompt or IP Adapter
|
||||||
|
if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
|
||||||
|
}
|
||||||
|
l.ipAdapters.forEach((ipAdapter) => {
|
||||||
|
// Must have model
|
||||||
|
if (!ipAdapter.model) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
||||||
|
}
|
||||||
|
// Model base must match
|
||||||
|
if (ipAdapter.model?.base !== model?.base) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
|
||||||
|
}
|
||||||
|
// Must have an image
|
||||||
|
if (!ipAdapter.image) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!fieldTemplate) {
|
if (problems.length) {
|
||||||
reasons.push(i18n.t('parameters.invoke.missingFieldTemplate'));
|
const content = upperFirst(problems.join(', '));
|
||||||
return;
|
reasons.push({ prefix, content });
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Handling for all other tabs
|
||||||
|
selectControlAdapterAll(controlAdapters)
|
||||||
|
.filter((ca) => ca.isEnabled)
|
||||||
|
.forEach((ca, i) => {
|
||||||
|
if (!ca.isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (fieldTemplate.required && field.value === undefined && !hasConnection) {
|
if (!ca.model) {
|
||||||
reasons.push(
|
reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) });
|
||||||
i18n.t('parameters.invoke.missingInputForField', {
|
} else if (ca.model.base !== model?.base) {
|
||||||
nodeLabel: node.data.label || nodeTemplate.title,
|
// This should never happen, just a sanity check
|
||||||
fieldLabel: field.label || fieldTemplate.title,
|
reasons.push({
|
||||||
})
|
content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }),
|
||||||
);
|
});
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
});
|
if (
|
||||||
});
|
!ca.controlImage ||
|
||||||
}
|
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
|
||||||
} else {
|
) {
|
||||||
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
|
reasons.push({
|
||||||
reasons.push(i18n.t('parameters.invoke.noPrompts'));
|
content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!model) {
|
return { isReady: !reasons.length, reasons };
|
||||||
reasons.push(i18n.t('parameters.invoke.noModelSelected'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTabName === 'generation') {
|
|
||||||
// Handling for generation tab
|
|
||||||
controlLayers.present.layers
|
|
||||||
.filter((l) => l.isEnabled)
|
|
||||||
.flatMap((l) => {
|
|
||||||
if (l.type === 'control_adapter_layer') {
|
|
||||||
return l.controlAdapter;
|
|
||||||
} else if (l.type === 'ip_adapter_layer') {
|
|
||||||
return l.ipAdapter;
|
|
||||||
} else if (l.type === 'regional_guidance_layer') {
|
|
||||||
return l.ipAdapters;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
})
|
|
||||||
.forEach((ca, i) => {
|
|
||||||
const hasNoModel = !ca.model;
|
|
||||||
const mismatchedModelBase = ca.model?.base !== model?.base;
|
|
||||||
const hasNoImage = !ca.image;
|
|
||||||
const imageNotProcessed =
|
|
||||||
(ca.type === 'controlnet' || ca.type === 't2i_adapter') && !ca.processedImage && ca.processorConfig;
|
|
||||||
|
|
||||||
if (hasNoModel) {
|
|
||||||
reasons.push(
|
|
||||||
i18n.t('parameters.invoke.noModelForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (mismatchedModelBase) {
|
|
||||||
// This should never happen, just a sanity check
|
|
||||||
reasons.push(
|
|
||||||
i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (hasNoImage) {
|
|
||||||
reasons.push(
|
|
||||||
i18n.t('parameters.invoke.noControlImageForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (imageNotProcessed) {
|
|
||||||
reasons.push(
|
|
||||||
i18n.t('parameters.invoke.imageNotProcessedForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Handling for all other tabs
|
|
||||||
selectControlAdapterAll(controlAdapters)
|
|
||||||
.filter((ca) => ca.isEnabled)
|
|
||||||
.forEach((ca, i) => {
|
|
||||||
if (!ca.isEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ca.model) {
|
|
||||||
reasons.push(
|
|
||||||
i18n.t('parameters.invoke.noModelForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (ca.model.base !== model?.base) {
|
|
||||||
// This should never happen, just a sanity check
|
|
||||||
reasons.push(
|
|
||||||
i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!ca.controlImage ||
|
|
||||||
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
|
|
||||||
) {
|
|
||||||
reasons.push(
|
|
||||||
i18n.t('parameters.invoke.noControlImageForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return { isReady: !reasons.length, reasons };
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const useIsReadyToEnqueue = () => {
|
export const useIsReadyToEnqueue = () => {
|
||||||
const { isReady, reasons } = useAppSelector(selector);
|
const templates = useStore($templates);
|
||||||
return { isReady, reasons };
|
const selector = useMemo(() => createSelector(templates), [templates]);
|
||||||
|
const value = useAppSelector(selector);
|
||||||
|
return value;
|
||||||
};
|
};
|
||||||
|
@ -21,8 +21,6 @@ import {
|
|||||||
setShouldShowBoundingBox,
|
setShouldShowBoundingBox,
|
||||||
} from 'features/canvas/store/canvasSlice';
|
} from 'features/canvas/store/canvasSlice';
|
||||||
import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
|
import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
|
||||||
import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes';
|
|
||||||
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -217,110 +215,107 @@ const IAICanvasToolbar = () => {
|
|||||||
[dispatch, isMaskEnabled]
|
[dispatch, isMaskEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]);
|
const layerOptions = useMemo<{ label: string; value: CanvasLayer }[]>(
|
||||||
|
() => [
|
||||||
|
{ label: t('unifiedCanvas.base'), value: 'base' },
|
||||||
|
{ label: t('unifiedCanvas.mask'), value: 'mask' },
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
const layerValue = useMemo(() => layerOptions.filter((o) => o.value === layer)[0] ?? null, [layer, layerOptions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" gap={2} alignItems="center">
|
<Flex alignItems="center" gap={2} flexWrap="wrap">
|
||||||
<Flex flex={1} justifyContent="center">
|
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
|
||||||
<Flex gap={2} marginInlineEnd="auto" />
|
<FormControl isDisabled={isStaging} w="5rem">
|
||||||
</Flex>
|
<Combobox value={layerValue} options={layerOptions} onChange={handleChangeLayer} />
|
||||||
<Flex flex={1} gap={2} justifyContent="center">
|
</FormControl>
|
||||||
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
|
</Tooltip>
|
||||||
<FormControl isDisabled={isStaging} w="5rem">
|
|
||||||
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} />
|
|
||||||
</FormControl>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<IAICanvasMaskOptions />
|
<IAICanvasMaskOptions />
|
||||||
<IAICanvasToolChooserOptions />
|
<IAICanvasToolChooserOptions />
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.move')} (V)`}
|
aria-label={`${t('unifiedCanvas.move')} (V)`}
|
||||||
tooltip={`${t('unifiedCanvas.move')} (V)`}
|
tooltip={`${t('unifiedCanvas.move')} (V)`}
|
||||||
icon={<PiHandGrabbingBold />}
|
icon={<PiHandGrabbingBold />}
|
||||||
isChecked={tool === 'move' || isStaging}
|
isChecked={tool === 'move' || isStaging}
|
||||||
onClick={handleSelectMoveTool}
|
onClick={handleSelectMoveTool}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
||||||
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
||||||
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
|
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
|
||||||
onClick={handleSetShouldShowBoundingBox}
|
onClick={handleSetShouldShowBoundingBox}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
|
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
|
||||||
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
|
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
|
||||||
icon={<PiCrosshairSimpleBold />}
|
icon={<PiCrosshairSimpleBold />}
|
||||||
onClick={handleClickResetCanvasView}
|
onClick={handleClickResetCanvasView}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
<IconButton
|
||||||
|
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
||||||
|
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
||||||
|
icon={<PiStackBold />}
|
||||||
|
onClick={handleMergeVisible}
|
||||||
|
isDisabled={isStaging}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||||
|
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||||
|
icon={<PiFloppyDiskBold />}
|
||||||
|
onClick={handleSaveToGallery}
|
||||||
|
isDisabled={isStaging}
|
||||||
|
/>
|
||||||
|
{isClipboardAPIAvailable && (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||||
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||||
icon={<PiStackBold />}
|
icon={<PiCopyBold />}
|
||||||
onClick={handleMergeVisible}
|
onClick={handleCopyImageToClipboard}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
)}
|
||||||
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
<IconButton
|
||||||
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
icon={<PiFloppyDiskBold />}
|
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
onClick={handleSaveToGallery}
|
icon={<PiDownloadSimpleBold />}
|
||||||
isDisabled={isStaging}
|
onClick={handleDownloadAsImage}
|
||||||
/>
|
isDisabled={isStaging}
|
||||||
{isClipboardAPIAvailable && (
|
/>
|
||||||
<IconButton
|
</ButtonGroup>
|
||||||
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
<ButtonGroup>
|
||||||
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
<IAICanvasUndoButton />
|
||||||
icon={<PiCopyBold />}
|
<IAICanvasRedoButton />
|
||||||
onClick={handleCopyImageToClipboard}
|
</ButtonGroup>
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
|
||||||
icon={<PiDownloadSimpleBold />}
|
|
||||||
onClick={handleDownloadAsImage}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
<ButtonGroup>
|
|
||||||
<IAICanvasUndoButton />
|
|
||||||
<IAICanvasRedoButton />
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('common.upload')}`}
|
aria-label={`${t('common.upload')}`}
|
||||||
tooltip={`${t('common.upload')}`}
|
tooltip={`${t('common.upload')}`}
|
||||||
icon={<PiUploadSimpleBold />}
|
icon={<PiUploadSimpleBold />}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
{...getUploadButtonProps()}
|
{...getUploadButtonProps()}
|
||||||
/>
|
/>
|
||||||
<input {...getUploadInputProps()} />
|
<input {...getUploadInputProps()} />
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
||||||
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
||||||
icon={<PiTrashSimpleBold />}
|
icon={<PiTrashSimpleBold />}
|
||||||
onClick={handleResetCanvas}
|
onClick={handleResetCanvas}
|
||||||
colorScheme="error"
|
colorScheme="error"
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IAICanvasSettingsButtonPopover />
|
<IAICanvasSettingsButtonPopover />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Flex>
|
|
||||||
<Flex flex={1} justifyContent="center">
|
|
||||||
<Flex gap={2} marginInlineStart="auto">
|
|
||||||
<ViewerButton />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,11 +5,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
export type CanvasLayer = 'base' | 'mask';
|
export type CanvasLayer = 'base' | 'mask';
|
||||||
|
|
||||||
export const LAYER_NAMES_DICT: { label: string; value: CanvasLayer }[] = [
|
|
||||||
{ label: 'Base', value: 'base' },
|
|
||||||
{ label: 'Mask', value: 'mask' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const zBoundingBoxScaleMethod = z.enum(['none', 'auto', 'manual']);
|
const zBoundingBoxScaleMethod = z.enum(['none', 'auto', 'manual']);
|
||||||
export type BoundingBoxScaleMethod = z.infer<typeof zBoundingBoxScaleMethod>;
|
export type BoundingBoxScaleMethod = z.infer<typeof zBoundingBoxScaleMethod>;
|
||||||
export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod =>
|
export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod =>
|
||||||
|
@ -5,22 +5,7 @@ import type {
|
|||||||
ParameterT2IAdapterModel,
|
ParameterT2IAdapterModel,
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
import type { components } from 'services/api/schema';
|
import type { components } from 'services/api/schema';
|
||||||
import type {
|
import type { Invocation } from 'services/api/types';
|
||||||
CannyImageProcessorInvocation,
|
|
||||||
ColorMapImageProcessorInvocation,
|
|
||||||
ContentShuffleImageProcessorInvocation,
|
|
||||||
DepthAnythingImageProcessorInvocation,
|
|
||||||
DWOpenposeImageProcessorInvocation,
|
|
||||||
HedImageProcessorInvocation,
|
|
||||||
LineartAnimeImageProcessorInvocation,
|
|
||||||
LineartImageProcessorInvocation,
|
|
||||||
MediapipeFaceProcessorInvocation,
|
|
||||||
MidasDepthImageProcessorInvocation,
|
|
||||||
MlsdImageProcessorInvocation,
|
|
||||||
NormalbaeImageProcessorInvocation,
|
|
||||||
PidiImageProcessorInvocation,
|
|
||||||
ZoeDepthImageProcessorInvocation,
|
|
||||||
} from 'services/api/types';
|
|
||||||
import type { O } from 'ts-toolbelt';
|
import type { O } from 'ts-toolbelt';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@ -28,20 +13,20 @@ import { z } from 'zod';
|
|||||||
* Any ControlNet processor node
|
* Any ControlNet processor node
|
||||||
*/
|
*/
|
||||||
export type ControlAdapterProcessorNode =
|
export type ControlAdapterProcessorNode =
|
||||||
| CannyImageProcessorInvocation
|
| Invocation<'canny_image_processor'>
|
||||||
| ColorMapImageProcessorInvocation
|
| Invocation<'color_map_image_processor'>
|
||||||
| ContentShuffleImageProcessorInvocation
|
| Invocation<'content_shuffle_image_processor'>
|
||||||
| DepthAnythingImageProcessorInvocation
|
| Invocation<'depth_anything_image_processor'>
|
||||||
| HedImageProcessorInvocation
|
| Invocation<'hed_image_processor'>
|
||||||
| LineartAnimeImageProcessorInvocation
|
| Invocation<'lineart_anime_image_processor'>
|
||||||
| LineartImageProcessorInvocation
|
| Invocation<'lineart_image_processor'>
|
||||||
| MediapipeFaceProcessorInvocation
|
| Invocation<'mediapipe_face_processor'>
|
||||||
| MidasDepthImageProcessorInvocation
|
| Invocation<'midas_depth_image_processor'>
|
||||||
| MlsdImageProcessorInvocation
|
| Invocation<'mlsd_image_processor'>
|
||||||
| NormalbaeImageProcessorInvocation
|
| Invocation<'normalbae_image_processor'>
|
||||||
| DWOpenposeImageProcessorInvocation
|
| Invocation<'dw_openpose_image_processor'>
|
||||||
| PidiImageProcessorInvocation
|
| Invocation<'pidi_image_processor'>
|
||||||
| ZoeDepthImageProcessorInvocation;
|
| Invocation<'zoe_depth_image_processor'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any ControlNet processor type
|
* Any ControlNet processor type
|
||||||
@ -71,7 +56,7 @@ export const isControlAdapterProcessorType = (v: unknown): v is ControlAdapterPr
|
|||||||
* The Canny processor node, with parameters flagged as required
|
* The Canny processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredCannyImageProcessorInvocation = O.Required<
|
export type RequiredCannyImageProcessorInvocation = O.Required<
|
||||||
CannyImageProcessorInvocation,
|
Invocation<'canny_image_processor'>,
|
||||||
'type' | 'low_threshold' | 'high_threshold' | 'image_resolution' | 'detect_resolution'
|
'type' | 'low_threshold' | 'high_threshold' | 'image_resolution' | 'detect_resolution'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -79,7 +64,7 @@ export type RequiredCannyImageProcessorInvocation = O.Required<
|
|||||||
* The Color Map processor node, with parameters flagged as required
|
* The Color Map processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredColorMapImageProcessorInvocation = O.Required<
|
export type RequiredColorMapImageProcessorInvocation = O.Required<
|
||||||
ColorMapImageProcessorInvocation,
|
Invocation<'color_map_image_processor'>,
|
||||||
'type' | 'color_map_tile_size'
|
'type' | 'color_map_tile_size'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -87,7 +72,7 @@ export type RequiredColorMapImageProcessorInvocation = O.Required<
|
|||||||
* The ContentShuffle processor node, with parameters flagged as required
|
* The ContentShuffle processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredContentShuffleImageProcessorInvocation = O.Required<
|
export type RequiredContentShuffleImageProcessorInvocation = O.Required<
|
||||||
ContentShuffleImageProcessorInvocation,
|
Invocation<'content_shuffle_image_processor'>,
|
||||||
'type' | 'detect_resolution' | 'image_resolution' | 'w' | 'h' | 'f'
|
'type' | 'detect_resolution' | 'image_resolution' | 'w' | 'h' | 'f'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -95,7 +80,7 @@ export type RequiredContentShuffleImageProcessorInvocation = O.Required<
|
|||||||
* The DepthAnything processor node, with parameters flagged as required
|
* The DepthAnything processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredDepthAnythingImageProcessorInvocation = O.Required<
|
export type RequiredDepthAnythingImageProcessorInvocation = O.Required<
|
||||||
DepthAnythingImageProcessorInvocation,
|
Invocation<'depth_anything_image_processor'>,
|
||||||
'type' | 'model_size' | 'resolution' | 'offload'
|
'type' | 'model_size' | 'resolution' | 'offload'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -108,7 +93,7 @@ export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSiz
|
|||||||
* The HED processor node, with parameters flagged as required
|
* The HED processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredHedImageProcessorInvocation = O.Required<
|
export type RequiredHedImageProcessorInvocation = O.Required<
|
||||||
HedImageProcessorInvocation,
|
Invocation<'hed_image_processor'>,
|
||||||
'type' | 'detect_resolution' | 'image_resolution' | 'scribble'
|
'type' | 'detect_resolution' | 'image_resolution' | 'scribble'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -116,7 +101,7 @@ export type RequiredHedImageProcessorInvocation = O.Required<
|
|||||||
* The Lineart Anime processor node, with parameters flagged as required
|
* The Lineart Anime processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredLineartAnimeImageProcessorInvocation = O.Required<
|
export type RequiredLineartAnimeImageProcessorInvocation = O.Required<
|
||||||
LineartAnimeImageProcessorInvocation,
|
Invocation<'lineart_anime_image_processor'>,
|
||||||
'type' | 'detect_resolution' | 'image_resolution'
|
'type' | 'detect_resolution' | 'image_resolution'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -124,7 +109,7 @@ export type RequiredLineartAnimeImageProcessorInvocation = O.Required<
|
|||||||
* The Lineart processor node, with parameters flagged as required
|
* The Lineart processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredLineartImageProcessorInvocation = O.Required<
|
export type RequiredLineartImageProcessorInvocation = O.Required<
|
||||||
LineartImageProcessorInvocation,
|
Invocation<'lineart_image_processor'>,
|
||||||
'type' | 'detect_resolution' | 'image_resolution' | 'coarse'
|
'type' | 'detect_resolution' | 'image_resolution' | 'coarse'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -132,7 +117,7 @@ export type RequiredLineartImageProcessorInvocation = O.Required<
|
|||||||
* The MediapipeFace processor node, with parameters flagged as required
|
* The MediapipeFace processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredMediapipeFaceProcessorInvocation = O.Required<
|
export type RequiredMediapipeFaceProcessorInvocation = O.Required<
|
||||||
MediapipeFaceProcessorInvocation,
|
Invocation<'mediapipe_face_processor'>,
|
||||||
'type' | 'max_faces' | 'min_confidence' | 'image_resolution' | 'detect_resolution'
|
'type' | 'max_faces' | 'min_confidence' | 'image_resolution' | 'detect_resolution'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -140,7 +125,7 @@ export type RequiredMediapipeFaceProcessorInvocation = O.Required<
|
|||||||
* The MidasDepth processor node, with parameters flagged as required
|
* The MidasDepth processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredMidasDepthImageProcessorInvocation = O.Required<
|
export type RequiredMidasDepthImageProcessorInvocation = O.Required<
|
||||||
MidasDepthImageProcessorInvocation,
|
Invocation<'midas_depth_image_processor'>,
|
||||||
'type' | 'a_mult' | 'bg_th' | 'image_resolution' | 'detect_resolution'
|
'type' | 'a_mult' | 'bg_th' | 'image_resolution' | 'detect_resolution'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -148,7 +133,7 @@ export type RequiredMidasDepthImageProcessorInvocation = O.Required<
|
|||||||
* The MLSD processor node, with parameters flagged as required
|
* The MLSD processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredMlsdImageProcessorInvocation = O.Required<
|
export type RequiredMlsdImageProcessorInvocation = O.Required<
|
||||||
MlsdImageProcessorInvocation,
|
Invocation<'mlsd_image_processor'>,
|
||||||
'type' | 'detect_resolution' | 'image_resolution' | 'thr_v' | 'thr_d'
|
'type' | 'detect_resolution' | 'image_resolution' | 'thr_v' | 'thr_d'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -156,7 +141,7 @@ export type RequiredMlsdImageProcessorInvocation = O.Required<
|
|||||||
* The NormalBae processor node, with parameters flagged as required
|
* The NormalBae processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredNormalbaeImageProcessorInvocation = O.Required<
|
export type RequiredNormalbaeImageProcessorInvocation = O.Required<
|
||||||
NormalbaeImageProcessorInvocation,
|
Invocation<'normalbae_image_processor'>,
|
||||||
'type' | 'detect_resolution' | 'image_resolution'
|
'type' | 'detect_resolution' | 'image_resolution'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -164,7 +149,7 @@ export type RequiredNormalbaeImageProcessorInvocation = O.Required<
|
|||||||
* The DW Openpose processor node, with parameters flagged as required
|
* The DW Openpose processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredDWOpenposeImageProcessorInvocation = O.Required<
|
export type RequiredDWOpenposeImageProcessorInvocation = O.Required<
|
||||||
DWOpenposeImageProcessorInvocation,
|
Invocation<'dw_openpose_image_processor'>,
|
||||||
'type' | 'image_resolution' | 'draw_body' | 'draw_face' | 'draw_hands'
|
'type' | 'image_resolution' | 'draw_body' | 'draw_face' | 'draw_hands'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -172,14 +157,14 @@ export type RequiredDWOpenposeImageProcessorInvocation = O.Required<
|
|||||||
* The Pidi processor node, with parameters flagged as required
|
* The Pidi processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredPidiImageProcessorInvocation = O.Required<
|
export type RequiredPidiImageProcessorInvocation = O.Required<
|
||||||
PidiImageProcessorInvocation,
|
Invocation<'pidi_image_processor'>,
|
||||||
'type' | 'detect_resolution' | 'image_resolution' | 'safe' | 'scribble'
|
'type' | 'detect_resolution' | 'image_resolution' | 'safe' | 'scribble'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ZoeDepth processor node, with parameters flagged as required
|
* The ZoeDepth processor node, with parameters flagged as required
|
||||||
*/
|
*/
|
||||||
export type RequiredZoeDepthImageProcessorInvocation = O.Required<ZoeDepthImageProcessorInvocation, 'type'>;
|
export type RequiredZoeDepthImageProcessorInvocation = O.Required<Invocation<'zoe_depth_image_processor'>, 'type'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any ControlNet Processor node, with its parameters flagged as required
|
* Any ControlNet Processor node, with its parameters flagged as required
|
||||||
|
@ -18,7 +18,12 @@ export const AddLayerButton = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton as={Button} leftIcon={<PiPlusBold />} variant="ghost">
|
<MenuButton
|
||||||
|
as={Button}
|
||||||
|
leftIcon={<PiPlusBold />}
|
||||||
|
variant="ghost"
|
||||||
|
data-testid="control-layers-add-layer-menu-button"
|
||||||
|
>
|
||||||
{t('controlLayers.addLayer')}
|
{t('controlLayers.addLayer')}
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
|
@ -19,7 +19,6 @@ export const CALayer = memo(({ layerId }: Props) => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected);
|
const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected);
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
// Must be capture so that the layer is selected before deleting/resetting/etc
|
|
||||||
dispatch(layerSelected(layerId));
|
dispatch(layerSelected(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
@ -42,10 +42,10 @@ export const ControlAdapterImagePreview = memo(
|
|||||||
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
|
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
|
||||||
|
|
||||||
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
|
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
|
||||||
controlAdapter.image?.imageName ?? skipToken
|
controlAdapter.image?.name ?? skipToken
|
||||||
);
|
);
|
||||||
const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery(
|
const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery(
|
||||||
controlAdapter.processedImage?.imageName ?? skipToken
|
controlAdapter.processedImage?.name ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
|
const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
|
||||||
@ -124,7 +124,7 @@ export const ControlAdapterImagePreview = memo(
|
|||||||
controlImage &&
|
controlImage &&
|
||||||
processedControlImage &&
|
processedControlImage &&
|
||||||
!isMouseOverImage &&
|
!isMouseOverImage &&
|
||||||
!controlAdapter.isProcessingImage &&
|
!controlAdapter.processorPendingBatchId &&
|
||||||
controlAdapter.processorConfig !== null;
|
controlAdapter.processorConfig !== null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -190,7 +190,7 @@ export const ControlAdapterImagePreview = memo(
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
{controlAdapter.isProcessingImage && (
|
{controlAdapter.processorPendingBatchId !== null && (
|
||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={0}
|
top={0}
|
||||||
|
@ -42,6 +42,7 @@ export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeM
|
|||||||
selectedModel,
|
selectedModel,
|
||||||
getIsDisabled,
|
getIsDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
groupByType: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -34,9 +34,7 @@ export const IPAdapterImagePreview = memo(
|
|||||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||||
const shift = useShiftModifier();
|
const shift = useShiftModifier();
|
||||||
|
|
||||||
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
|
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken);
|
||||||
image?.imageName ?? skipToken
|
|
||||||
);
|
|
||||||
const handleResetControlImage = useCallback(() => {
|
const handleResetControlImage = useCallback(() => {
|
||||||
onChangeImage(null);
|
onChangeImage(null);
|
||||||
}, [onChangeImage]);
|
}, [onChangeImage]);
|
||||||
|
@ -2,14 +2,13 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
|||||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
|
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
|
||||||
import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters';
|
import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters';
|
||||||
import { CA_PROCESSOR_DATA, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters';
|
import { isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ProcessorWrapper from './ProcessorWrapper';
|
import ProcessorWrapper from './ProcessorWrapper';
|
||||||
|
|
||||||
type Props = ProcessorComponentProps<DepthAnythingProcessorConfig>;
|
type Props = ProcessorComponentProps<DepthAnythingProcessorConfig>;
|
||||||
const DEFAULTS = CA_PROCESSOR_DATA['depth_anything_image_processor'].buildDefaults();
|
|
||||||
|
|
||||||
export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => {
|
export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -38,12 +37,7 @@ export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => {
|
|||||||
<ProcessorWrapper>
|
<ProcessorWrapper>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel m={0}>{t('controlnet.modelSize')}</FormLabel>
|
<FormLabel m={0}>{t('controlnet.modelSize')}</FormLabel>
|
||||||
<Combobox
|
<Combobox value={value} options={options} onChange={handleModelSizeChange} isSearchable={false} />
|
||||||
value={value}
|
|
||||||
defaultInputValue={DEFAULTS.model_size}
|
|
||||||
options={options}
|
|
||||||
onChange={handleModelSizeChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</ProcessorWrapper>
|
</ProcessorWrapper>
|
||||||
);
|
);
|
||||||
|
@ -32,7 +32,7 @@ export const ControlLayersPanelContent = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
{layerIdTypePairs.length > 0 && (
|
{layerIdTypePairs.length > 0 && (
|
||||||
<ScrollableContent>
|
<ScrollableContent>
|
||||||
<Flex flexDir="column" gap={2}>
|
<Flex flexDir="column" gap={2} data-testid="control-layers-layer-list">
|
||||||
{layerIdTypePairs.map(({ id, type }) => (
|
{layerIdTypePairs.map(({ id, type }) => (
|
||||||
<LayerWrapper key={id} id={id} type={type} />
|
<LayerWrapper key={id} id={id} type={type} />
|
||||||
))}
|
))}
|
||||||
|
@ -1,12 +1,30 @@
|
|||||||
import { Flex, IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { setShouldInvertBrushSizeScrollDirection } from 'features/canvas/store/canvasSlice';
|
||||||
import { GlobalMaskLayerOpacity } from 'features/controlLayers/components/GlobalMaskLayerOpacity';
|
import { GlobalMaskLayerOpacity } from 'features/controlLayers/components/GlobalMaskLayerOpacity';
|
||||||
import { memo } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RiSettings4Fill } from 'react-icons/ri';
|
import { RiSettings4Fill } from 'react-icons/ri';
|
||||||
|
|
||||||
const ControlLayersSettingsPopover = () => {
|
const ControlLayersSettingsPopover = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection);
|
||||||
|
const handleChangeShouldInvertBrushSizeScrollDirection = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Popover isLazy>
|
<Popover isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
@ -16,6 +34,13 @@ const ControlLayersSettingsPopover = () => {
|
|||||||
<PopoverBody>
|
<PopoverBody>
|
||||||
<Flex direction="column" gap={2}>
|
<Flex direction="column" gap={2}>
|
||||||
<GlobalMaskLayerOpacity />
|
<GlobalMaskLayerOpacity />
|
||||||
|
<FormControl w="full">
|
||||||
|
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
|
||||||
|
<Checkbox
|
||||||
|
isChecked={shouldInvertBrushSizeScrollDirection}
|
||||||
|
onChange={handleChangeShouldInvertBrushSizeScrollDirection}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
</Flex>
|
</Flex>
|
||||||
</PopoverBody>
|
</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
@ -4,14 +4,17 @@ import { BrushSize } from 'features/controlLayers/components/BrushSize';
|
|||||||
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
||||||
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
|
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
|
||||||
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
|
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
|
||||||
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
|
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||||
|
import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const ControlLayersToolbar = memo(() => {
|
export const ControlLayersToolbar = memo(() => {
|
||||||
return (
|
return (
|
||||||
<Flex w="full" gap={2}>
|
<Flex w="full" gap={2}>
|
||||||
<Flex flex={1} justifyContent="center">
|
<Flex flex={1} justifyContent="center">
|
||||||
<Flex gap={2} marginInlineEnd="auto" />
|
<Flex gap={2} marginInlineEnd="auto">
|
||||||
|
<ToggleProgressButton />
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} gap={2} justifyContent="center">
|
<Flex flex={1} gap={2} justifyContent="center">
|
||||||
<BrushSize />
|
<BrushSize />
|
||||||
@ -21,7 +24,7 @@ export const ControlLayersToolbar = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} justifyContent="center">
|
<Flex flex={1} justifyContent="center">
|
||||||
<Flex gap={2} marginInlineStart="auto">
|
<Flex gap={2} marginInlineStart="auto">
|
||||||
<ViewerButton />
|
<ViewerToggleMenu />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -20,6 +20,7 @@ export const DeleteAllLayersButton = memo(() => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
colorScheme="error"
|
colorScheme="error"
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
|
data-testid="control-layers-delete-all-layers-button"
|
||||||
>
|
>
|
||||||
{t('controlLayers.deleteAll')}
|
{t('controlLayers.deleteAll')}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -8,6 +8,7 @@ import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerT
|
|||||||
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
||||||
import {
|
import {
|
||||||
|
iiLayerDenoisingStrengthChanged,
|
||||||
iiLayerImageChanged,
|
iiLayerImageChanged,
|
||||||
layerSelected,
|
layerSelected,
|
||||||
selectIILayerOrThrow,
|
selectIILayerOrThrow,
|
||||||
@ -36,6 +37,13 @@ export const IILayer = memo(({ layerId }: Props) => {
|
|||||||
[dispatch, layerId]
|
[dispatch, layerId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onChangeDenoisingStrength = useCallback(
|
||||||
|
(denoisingStrength: number) => {
|
||||||
|
dispatch(iiLayerDenoisingStrengthChanged({ layerId, denoisingStrength }));
|
||||||
|
},
|
||||||
|
[dispatch, layerId]
|
||||||
|
);
|
||||||
|
|
||||||
const droppableData = useMemo<IILayerImageDropData>(
|
const droppableData = useMemo<IILayerImageDropData>(
|
||||||
() => ({
|
() => ({
|
||||||
actionType: 'SET_II_LAYER_IMAGE',
|
actionType: 'SET_II_LAYER_IMAGE',
|
||||||
@ -67,7 +75,7 @@ export const IILayer = memo(({ layerId }: Props) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
||||||
<ImageToImageStrength />
|
<ImageToImageStrength value={layer.denoisingStrength} onChange={onChangeDenoisingStrength} />
|
||||||
<InitialImagePreview
|
<InitialImagePreview
|
||||||
image={layer.image}
|
image={layer.image}
|
||||||
onChangeImage={onChangeImage}
|
onChangeImage={onChangeImage}
|
||||||
|
@ -32,7 +32,7 @@ export const InitialImagePreview = memo(({ image, onChangeImage, droppableData,
|
|||||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||||
const shift = useShiftModifier();
|
const shift = useShiftModifier();
|
||||||
|
|
||||||
const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.imageName ?? skipToken);
|
const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken);
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
onChangeImage(null);
|
onChangeImage(null);
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper';
|
import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper';
|
||||||
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
||||||
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
||||||
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
||||||
import { memo } from 'react';
|
import { layerSelected, selectIPALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IPALayer = memo(({ layerId }: Props) => {
|
export const IPALayer = memo(({ layerId }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const isSelected = useAppSelector((s) => selectIPALayerOrThrow(s.controlLayers.present, layerId).isSelected);
|
||||||
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
dispatch(layerSelected(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
return (
|
return (
|
||||||
<LayerWrapper borderColor="base.800">
|
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
|
||||||
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
|
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
|
||||||
<LayerVisibilityToggle layerId={layerId} />
|
<LayerVisibilityToggle layerId={layerId} />
|
||||||
<LayerTitle type="ip_adapter_layer" />
|
<LayerTitle type="ip_adapter_layer" />
|
||||||
|
@ -10,7 +10,16 @@ type Props = PropsWithChildren<{
|
|||||||
|
|
||||||
export const LayerWrapper = memo(({ onClick, borderColor, children }: Props) => {
|
export const LayerWrapper = memo(({ onClick, borderColor, children }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} onClick={onClick} bg={borderColor} px={2} borderRadius="base" py="1px">
|
<Flex
|
||||||
|
gap={2}
|
||||||
|
onClick={onClick}
|
||||||
|
bg={borderColor}
|
||||||
|
px={2}
|
||||||
|
borderRadius="base"
|
||||||
|
py="1px"
|
||||||
|
transitionProperty="all"
|
||||||
|
transitionDuration="0.2s"
|
||||||
|
>
|
||||||
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
|
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
|
||||||
{children}
|
{children}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -45,6 +45,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => {
|
|||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
paddingRight={30}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<RGLayerPromptDeleteButton layerId={layerId} polarity="negative" />
|
<RGLayerPromptDeleteButton layerId={layerId} polarity="negative" />
|
||||||
|
@ -45,6 +45,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => {
|
|||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
paddingRight={30}
|
||||||
minH={28}
|
minH={28}
|
||||||
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<RGLayerPromptDeleteButton layerId={layerId} polarity="positive" />
|
<RGLayerPromptDeleteButton layerId={layerId} polarity="positive" />
|
||||||
|
@ -130,11 +130,11 @@ const useStageRenderer = (
|
|||||||
}, [stage, state.size.width, state.size.height, wrapper]);
|
}, [stage, state.size.width, state.size.height, wrapper]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering tool preview');
|
|
||||||
if (asPreview) {
|
if (asPreview) {
|
||||||
// Preview should not display tool
|
// Preview should not display tool
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
log.trace('Rendering tool preview');
|
||||||
renderers.renderToolPreview(
|
renderers.renderToolPreview(
|
||||||
stage,
|
stage,
|
||||||
tool,
|
tool,
|
||||||
@ -178,15 +178,24 @@ const useStageRenderer = (
|
|||||||
// Preview should not display bboxes
|
// Preview should not display bboxes
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderers.renderBbox(stage, state.layers, tool, onBboxChanged);
|
renderers.renderBboxes(stage, state.layers, tool);
|
||||||
}, [stage, asPreview, state.layers, tool, onBboxChanged, renderers]);
|
}, [stage, asPreview, state.layers, tool, onBboxChanged, renderers]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering background');
|
if (asPreview) {
|
||||||
|
// Preview should not check for transparency
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.trace('Updating bboxes');
|
||||||
|
debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged);
|
||||||
|
}, [stage, asPreview, state.layers, onBboxChanged]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
if (asPreview) {
|
if (asPreview) {
|
||||||
// The preview should not have a background
|
// The preview should not have a background
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
log.trace('Rendering background');
|
||||||
renderers.renderBackground(stage, state.size.width, state.size.height);
|
renderers.renderBackground(stage, state.size.width, state.size.height);
|
||||||
}, [stage, asPreview, state.size.width, state.size.height, renderers]);
|
}, [stage, asPreview, state.size.width, state.size.height, renderers]);
|
||||||
|
|
||||||
@ -196,11 +205,11 @@ const useStageRenderer = (
|
|||||||
}, [stage, layerIds, renderers]);
|
}, [stage, layerIds, renderers]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering no layers message');
|
|
||||||
if (asPreview) {
|
if (asPreview) {
|
||||||
// The preview should not display the no layers message
|
// The preview should not display the no layers message
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
log.trace('Rendering no layers message');
|
||||||
renderers.renderNoLayersMessage(stage, layerCount, state.size.width, state.size.height);
|
renderers.renderNoLayersMessage(stage, layerCount, state.size.width, state.size.height);
|
||||||
}, [stage, layerCount, renderers, asPreview, state.size.width, state.size.height]);
|
}, [stage, layerCount, renderers, asPreview, state.size.width, state.size.height]);
|
||||||
|
|
||||||
@ -233,7 +242,14 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Flex overflow="hidden" w="full" h="full">
|
<Flex overflow="hidden" w="full" h="full">
|
||||||
<Flex ref={wrapperRef} w="full" h="full" alignItems="center" justifyContent="center">
|
<Flex ref={wrapperRef} w="full" h="full" alignItems="center" justifyContent="center">
|
||||||
<Flex ref={containerRef} tabIndex={-1} bg="base.850" borderRadius="base" overflow="hidden" />
|
<Flex
|
||||||
|
ref={containerRef}
|
||||||
|
tabIndex={-1}
|
||||||
|
bg="base.850"
|
||||||
|
borderRadius="base"
|
||||||
|
overflow="hidden"
|
||||||
|
data-testid="control-layers-canvas"
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -188,15 +188,15 @@ export const useMouseEvents = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pos = syncCursorPos(stage);
|
const pos = syncCursorPos(stage);
|
||||||
|
$isDrawing.set(false);
|
||||||
|
$lastCursorPos.set(null);
|
||||||
|
$lastMouseDownPos.set(null);
|
||||||
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
|
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) {
|
if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) {
|
||||||
dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: [pos.x, pos.y] }));
|
dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: [pos.x, pos.y] }));
|
||||||
}
|
}
|
||||||
$isDrawing.set(false);
|
|
||||||
$lastCursorPos.set(null);
|
|
||||||
$lastMouseDownPos.set(null);
|
|
||||||
},
|
},
|
||||||
[selectedLayerId, selectedLayerType, tool, dispatch]
|
[selectedLayerId, selectedLayerType, tool, dispatch]
|
||||||
);
|
);
|
||||||
@ -224,5 +224,10 @@ export const useMouseEvents = () => {
|
|||||||
[selectedLayerType, tool, shouldInvertBrushSizeScrollDirection, dispatch, brushSize]
|
[selectedLayerType, tool, shouldInvertBrushSizeScrollDirection, dispatch, brushSize]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel };
|
const handlers = useMemo(
|
||||||
|
() => ({ onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel }),
|
||||||
|
[onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel]
|
||||||
|
);
|
||||||
|
|
||||||
|
return handlers;
|
||||||
};
|
};
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import {
|
|
||||||
isControlAdapterLayer,
|
|
||||||
isInitialImageLayer,
|
|
||||||
isIPAdapterLayer,
|
|
||||||
isRegionalGuidanceLayer,
|
|
||||||
selectControlLayersSlice,
|
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => {
|
|
||||||
let count = 0;
|
|
||||||
controlLayers.present.layers.forEach((l) => {
|
|
||||||
if (isRegionalGuidanceLayer(l)) {
|
|
||||||
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
|
||||||
const hasAtLeastOneImagePrompt = l.ipAdapters.filter((ipa) => Boolean(ipa.image)).length > 0;
|
|
||||||
if (hasTextPrompt || hasAtLeastOneImagePrompt) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isControlAdapterLayer(l)) {
|
|
||||||
if (l.controlAdapter.image || l.controlAdapter.processedImage) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isIPAdapterLayer(l)) {
|
|
||||||
if (l.ipAdapter.image) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isInitialImageLayer(l)) {
|
|
||||||
if (l.image) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return count;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useControlLayersTitle = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const validLayerCount = useAppSelector(selectValidLayerCount);
|
|
||||||
const title = useMemo(() => {
|
|
||||||
const suffix = validLayerCount > 0 ? ` (${validLayerCount})` : '';
|
|
||||||
return `${t('controlLayers.controlLayers')}${suffix}`;
|
|
||||||
}, [t, validLayerCount]);
|
|
||||||
return title;
|
|
||||||
};
|
|
@ -27,7 +27,7 @@ import { modelChanged } from 'features/parameters/store/generationSlice';
|
|||||||
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
|
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
|
||||||
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import { isEqual, partition } from 'lodash-es';
|
import { isEqual, partition, unset } from 'lodash-es';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import type { RgbColor } from 'react-colorful';
|
import type { RgbColor } from 'react-colorful';
|
||||||
import type { UndoableOptions } from 'redux-undo';
|
import type { UndoableOptions } from 'redux-undo';
|
||||||
@ -49,7 +49,7 @@ import type {
|
|||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const initialControlLayersState: ControlLayersState = {
|
export const initialControlLayersState: ControlLayersState = {
|
||||||
_version: 1,
|
_version: 3,
|
||||||
selectedLayerId: null,
|
selectedLayerId: null,
|
||||||
brushSize: 100,
|
brushSize: 100,
|
||||||
layers: [],
|
layers: [],
|
||||||
@ -124,6 +124,12 @@ const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => {
|
|||||||
const lastColor = rgLayers[rgLayers.length - 1]?.previewColor;
|
const lastColor = rgLayers[rgLayers.length - 1]?.previewColor;
|
||||||
return LayerColors.next(lastColor);
|
return LayerColors.next(lastColor);
|
||||||
};
|
};
|
||||||
|
const exclusivelySelectLayer = (state: ControlLayersState, layerId: string) => {
|
||||||
|
for (const layer of state.layers) {
|
||||||
|
layer.isSelected = layer.id === layerId;
|
||||||
|
}
|
||||||
|
state.selectedLayerId = layerId;
|
||||||
|
};
|
||||||
|
|
||||||
export const controlLayersSlice = createSlice({
|
export const controlLayersSlice = createSlice({
|
||||||
name: 'controlLayers',
|
name: 'controlLayers',
|
||||||
@ -131,14 +137,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
reducers: {
|
reducers: {
|
||||||
//#region Any Layer Type
|
//#region Any Layer Type
|
||||||
layerSelected: (state, action: PayloadAction<string>) => {
|
layerSelected: (state, action: PayloadAction<string>) => {
|
||||||
for (const layer of state.layers.filter(isRenderableLayer)) {
|
exclusivelySelectLayer(state, action.payload);
|
||||||
if (layer.id === action.payload) {
|
|
||||||
layer.isSelected = true;
|
|
||||||
state.selectedLayerId = action.payload;
|
|
||||||
} else {
|
|
||||||
layer.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
layerVisibilityToggled: (state, action: PayloadAction<string>) => {
|
layerVisibilityToggled: (state, action: PayloadAction<string>) => {
|
||||||
const layer = state.layers.find((l) => l.id === action.payload);
|
const layer = state.layers.find((l) => l.id === action.payload);
|
||||||
@ -167,7 +166,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
|
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
|
||||||
layer.maskObjects = [];
|
layer.maskObjects = [];
|
||||||
layer.uploadedMaskImage = null;
|
layer.uploadedMaskImage = null;
|
||||||
layer.needsPixelBbox = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -178,7 +176,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
layer.maskObjects = [];
|
layer.maskObjects = [];
|
||||||
layer.bbox = null;
|
layer.bbox = null;
|
||||||
layer.isEnabled = true;
|
layer.isEnabled = true;
|
||||||
layer.needsPixelBbox = false;
|
|
||||||
layer.bboxNeedsUpdate = false;
|
layer.bboxNeedsUpdate = false;
|
||||||
layer.uploadedMaskImage = null;
|
layer.uploadedMaskImage = null;
|
||||||
}
|
}
|
||||||
@ -244,17 +241,16 @@ export const controlLayersSlice = createSlice({
|
|||||||
controlAdapter,
|
controlAdapter,
|
||||||
};
|
};
|
||||||
state.layers.push(layer);
|
state.layers.push(layer);
|
||||||
state.selectedLayerId = layer.id;
|
exclusivelySelectLayer(state, layer.id);
|
||||||
for (const layer of state.layers.filter(isRenderableLayer)) {
|
|
||||||
if (layer.id !== layerId) {
|
|
||||||
layer.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({
|
prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({
|
||||||
payload: { layerId: uuidv4(), controlAdapter },
|
payload: { layerId: uuidv4(), controlAdapter },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
caLayerRecalled: (state, action: PayloadAction<ControlAdapterLayer>) => {
|
||||||
|
state.layers.push({ ...action.payload, isSelected: true });
|
||||||
|
exclusivelySelectLayer(state, action.payload.id);
|
||||||
|
},
|
||||||
caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectCALayerOrThrow(state, layerId);
|
||||||
@ -338,19 +334,13 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectCALayerOrThrow(state, layerId);
|
||||||
layer.opacity = opacity;
|
layer.opacity = opacity;
|
||||||
},
|
},
|
||||||
caLayerIsProcessingImageChanged: (
|
caLayerProcessorPendingBatchIdChanged: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ layerId: string; isProcessingImage: boolean }>
|
action: PayloadAction<{ layerId: string; batchId: string | null }>
|
||||||
) => {
|
) => {
|
||||||
const { layerId, isProcessingImage } = action.payload;
|
const { layerId, batchId } = action.payload;
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectCALayerOrThrow(state, layerId);
|
||||||
layer.controlAdapter.isProcessingImage = isProcessingImage;
|
layer.controlAdapter.processorPendingBatchId = batchId;
|
||||||
},
|
|
||||||
caLayerControlNetsDeleted: (state) => {
|
|
||||||
state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 'controlnet');
|
|
||||||
},
|
|
||||||
caLayerT2IAdaptersDeleted: (state) => {
|
|
||||||
state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 't2i_adapter');
|
|
||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
@ -362,12 +352,17 @@ export const controlLayersSlice = createSlice({
|
|||||||
id: getIPALayerId(layerId),
|
id: getIPALayerId(layerId),
|
||||||
type: 'ip_adapter_layer',
|
type: 'ip_adapter_layer',
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
isSelected: true,
|
||||||
ipAdapter,
|
ipAdapter,
|
||||||
};
|
};
|
||||||
state.layers.push(layer);
|
state.layers.push(layer);
|
||||||
|
exclusivelySelectLayer(state, layer.id);
|
||||||
},
|
},
|
||||||
prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }),
|
prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }),
|
||||||
},
|
},
|
||||||
|
ipaLayerRecalled: (state, action: PayloadAction<IPAdapterLayer>) => {
|
||||||
|
state.layers.push(action.payload);
|
||||||
|
},
|
||||||
ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
const layer = selectIPALayerOrThrow(state, layerId);
|
const layer = selectIPALayerOrThrow(state, layerId);
|
||||||
@ -401,9 +396,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = selectIPALayerOrThrow(state, layerId);
|
const layer = selectIPALayerOrThrow(state, layerId);
|
||||||
layer.ipAdapter.clipVisionModel = clipVisionModel;
|
layer.ipAdapter.clipVisionModel = clipVisionModel;
|
||||||
},
|
},
|
||||||
ipaLayersDeleted: (state) => {
|
|
||||||
state.layers = state.layers.filter((l) => !isIPAdapterLayer(l));
|
|
||||||
},
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region CA or IPA Layers
|
//#region CA or IPA Layers
|
||||||
@ -445,7 +437,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
autoNegative: 'invert',
|
autoNegative: 'invert',
|
||||||
needsPixelBbox: false,
|
|
||||||
positivePrompt: '',
|
positivePrompt: '',
|
||||||
negativePrompt: null,
|
negativePrompt: null,
|
||||||
ipAdapters: [],
|
ipAdapters: [],
|
||||||
@ -453,15 +444,14 @@ export const controlLayersSlice = createSlice({
|
|||||||
uploadedMaskImage: null,
|
uploadedMaskImage: null,
|
||||||
};
|
};
|
||||||
state.layers.push(layer);
|
state.layers.push(layer);
|
||||||
state.selectedLayerId = layer.id;
|
exclusivelySelectLayer(state, layer.id);
|
||||||
for (const layer of state.layers.filter(isRenderableLayer)) {
|
|
||||||
if (layer.id !== layerId) {
|
|
||||||
layer.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
prepare: () => ({ payload: { layerId: uuidv4() } }),
|
prepare: () => ({ payload: { layerId: uuidv4() } }),
|
||||||
},
|
},
|
||||||
|
rgLayerRecalled: (state, action: PayloadAction<RegionalGuidanceLayer>) => {
|
||||||
|
state.layers.push({ ...action.payload, isSelected: true });
|
||||||
|
exclusivelySelectLayer(state, action.payload.id);
|
||||||
|
},
|
||||||
rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
||||||
const { layerId, prompt } = action.payload;
|
const { layerId, prompt } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectRGLayerOrThrow(state, layerId);
|
||||||
@ -501,9 +491,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
});
|
});
|
||||||
layer.bboxNeedsUpdate = true;
|
layer.bboxNeedsUpdate = true;
|
||||||
layer.uploadedMaskImage = null;
|
layer.uploadedMaskImage = null;
|
||||||
if (!layer.needsPixelBbox && tool === 'eraser') {
|
|
||||||
layer.needsPixelBbox = true;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
prepare: (payload: { layerId: string; points: [number, number, number, number]; tool: DrawingTool }) => ({
|
prepare: (payload: { layerId: string; points: [number, number, number, number]; tool: DrawingTool }) => ({
|
||||||
payload: { ...payload, lineUuid: uuidv4() },
|
payload: { ...payload, lineUuid: uuidv4() },
|
||||||
@ -642,16 +629,17 @@ export const controlLayersSlice = createSlice({
|
|||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
|
denoisingStrength: 0.75,
|
||||||
};
|
};
|
||||||
state.layers.push(layer);
|
state.layers.push(layer);
|
||||||
state.selectedLayerId = layer.id;
|
exclusivelySelectLayer(state, layer.id);
|
||||||
for (const layer of state.layers.filter(isRenderableLayer)) {
|
|
||||||
if (layer.id !== layerId) {
|
|
||||||
layer.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: 'initial_image_layer', imageDTO } }),
|
prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: INITIAL_IMAGE_LAYER_ID, imageDTO } }),
|
||||||
|
},
|
||||||
|
iiLayerRecalled: (state, action: PayloadAction<InitialImageLayer>) => {
|
||||||
|
state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true));
|
||||||
|
state.layers.push({ ...action.payload, isSelected: true });
|
||||||
|
exclusivelySelectLayer(state, action.payload.id);
|
||||||
},
|
},
|
||||||
iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
@ -666,6 +654,11 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = selectIILayerOrThrow(state, layerId);
|
const layer = selectIILayerOrThrow(state, layerId);
|
||||||
layer.opacity = opacity;
|
layer.opacity = opacity;
|
||||||
},
|
},
|
||||||
|
iiLayerDenoisingStrengthChanged: (state, action: PayloadAction<{ layerId: string; denoisingStrength: number }>) => {
|
||||||
|
const { layerId, denoisingStrength } = action.payload;
|
||||||
|
const layer = selectIILayerOrThrow(state, layerId);
|
||||||
|
layer.denoisingStrength = denoisingStrength;
|
||||||
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Globals
|
//#region Globals
|
||||||
@ -799,6 +792,7 @@ export const {
|
|||||||
allLayersDeleted,
|
allLayersDeleted,
|
||||||
// CA Layers
|
// CA Layers
|
||||||
caLayerAdded,
|
caLayerAdded,
|
||||||
|
caLayerRecalled,
|
||||||
caLayerImageChanged,
|
caLayerImageChanged,
|
||||||
caLayerProcessedImageChanged,
|
caLayerProcessedImageChanged,
|
||||||
caLayerModelChanged,
|
caLayerModelChanged,
|
||||||
@ -806,21 +800,20 @@ export const {
|
|||||||
caLayerProcessorConfigChanged,
|
caLayerProcessorConfigChanged,
|
||||||
caLayerIsFilterEnabledChanged,
|
caLayerIsFilterEnabledChanged,
|
||||||
caLayerOpacityChanged,
|
caLayerOpacityChanged,
|
||||||
caLayerIsProcessingImageChanged,
|
caLayerProcessorPendingBatchIdChanged,
|
||||||
caLayerControlNetsDeleted,
|
|
||||||
caLayerT2IAdaptersDeleted,
|
|
||||||
// IPA Layers
|
// IPA Layers
|
||||||
ipaLayerAdded,
|
ipaLayerAdded,
|
||||||
|
ipaLayerRecalled,
|
||||||
ipaLayerImageChanged,
|
ipaLayerImageChanged,
|
||||||
ipaLayerMethodChanged,
|
ipaLayerMethodChanged,
|
||||||
ipaLayerModelChanged,
|
ipaLayerModelChanged,
|
||||||
ipaLayerCLIPVisionModelChanged,
|
ipaLayerCLIPVisionModelChanged,
|
||||||
ipaLayersDeleted,
|
|
||||||
// CA or IPA Layers
|
// CA or IPA Layers
|
||||||
caOrIPALayerWeightChanged,
|
caOrIPALayerWeightChanged,
|
||||||
caOrIPALayerBeginEndStepPctChanged,
|
caOrIPALayerBeginEndStepPctChanged,
|
||||||
// RG Layers
|
// RG Layers
|
||||||
rgLayerAdded,
|
rgLayerAdded,
|
||||||
|
rgLayerRecalled,
|
||||||
rgLayerPositivePromptChanged,
|
rgLayerPositivePromptChanged,
|
||||||
rgLayerNegativePromptChanged,
|
rgLayerNegativePromptChanged,
|
||||||
rgLayerPreviewColorChanged,
|
rgLayerPreviewColorChanged,
|
||||||
@ -839,8 +832,10 @@ export const {
|
|||||||
rgLayerIPAdapterCLIPVisionModelChanged,
|
rgLayerIPAdapterCLIPVisionModelChanged,
|
||||||
// II Layer
|
// II Layer
|
||||||
iiLayerAdded,
|
iiLayerAdded,
|
||||||
|
iiLayerRecalled,
|
||||||
iiLayerImageChanged,
|
iiLayerImageChanged,
|
||||||
iiLayerOpacityChanged,
|
iiLayerOpacityChanged,
|
||||||
|
iiLayerDenoisingStrengthChanged,
|
||||||
// Globals
|
// Globals
|
||||||
positivePromptChanged,
|
positivePromptChanged,
|
||||||
negativePromptChanged,
|
negativePromptChanged,
|
||||||
@ -860,6 +855,19 @@ export const selectControlLayersSlice = (state: RootState) => state.controlLayer
|
|||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const migrateControlLayersState = (state: any): any => {
|
const migrateControlLayersState = (state: any): any => {
|
||||||
|
if (state._version === 1) {
|
||||||
|
// Reset state for users on v1 (e.g. beta users), some changes could cause
|
||||||
|
state = deepClone(initialControlLayersState);
|
||||||
|
}
|
||||||
|
if (state._version === 2) {
|
||||||
|
// The CA `isProcessingImage` flag was replaced with a `processorPendingBatchId` property, fix up CA layers
|
||||||
|
for (const layer of (state as ControlLayersState).layers) {
|
||||||
|
if (layer.type === 'control_adapter_layer') {
|
||||||
|
layer.controlAdapter.processorPendingBatchId = null;
|
||||||
|
unset(layer.controlAdapter, 'isProcessingImage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -886,21 +894,22 @@ export const RG_LAYER_NAME = 'regional_guidance_layer';
|
|||||||
export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line';
|
export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line';
|
||||||
export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group';
|
export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group';
|
||||||
export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect';
|
export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect';
|
||||||
|
export const INITIAL_IMAGE_LAYER_ID = 'singleton_initial_image_layer';
|
||||||
export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer';
|
export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer';
|
||||||
export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image';
|
export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image';
|
||||||
export const LAYER_BBOX_NAME = 'layer.bbox';
|
export const LAYER_BBOX_NAME = 'layer.bbox';
|
||||||
export const COMPOSITING_RECT_NAME = 'compositing-rect';
|
export const COMPOSITING_RECT_NAME = 'compositing-rect';
|
||||||
|
|
||||||
// Getters for non-singleton layer and object IDs
|
// Getters for non-singleton layer and object IDs
|
||||||
const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;
|
export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;
|
||||||
const getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
|
const getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
|
||||||
const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`;
|
const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`;
|
||||||
export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
|
export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
|
||||||
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
|
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
|
||||||
const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`;
|
export const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`;
|
||||||
export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
||||||
export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
||||||
const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`;
|
export const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`;
|
||||||
|
|
||||||
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {
|
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {
|
||||||
name: controlLayersSlice.name,
|
name: controlLayersSlice.name,
|
||||||
|
@ -1,90 +1,119 @@
|
|||||||
import type {
|
import {
|
||||||
ControlNetConfigV2,
|
zControlNetConfigV2,
|
||||||
ImageWithDims,
|
zImageWithDims,
|
||||||
IPAdapterConfigV2,
|
zIPAdapterConfigV2,
|
||||||
T2IAdapterConfigV2,
|
zT2IAdapterConfigV2,
|
||||||
} from 'features/controlLayers/util/controlAdapters';
|
} from 'features/controlLayers/util/controlAdapters';
|
||||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
import type {
|
import {
|
||||||
ParameterAutoNegative,
|
type ParameterHeight,
|
||||||
ParameterHeight,
|
type ParameterNegativePrompt,
|
||||||
ParameterNegativePrompt,
|
type ParameterNegativeStylePromptSDXL,
|
||||||
ParameterNegativeStylePromptSDXL,
|
type ParameterPositivePrompt,
|
||||||
ParameterPositivePrompt,
|
type ParameterPositiveStylePromptSDXL,
|
||||||
ParameterPositiveStylePromptSDXL,
|
type ParameterWidth,
|
||||||
ParameterWidth,
|
zAutoNegative,
|
||||||
|
zParameterNegativePrompt,
|
||||||
|
zParameterPositivePrompt,
|
||||||
|
zParameterStrength,
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import { z } from 'zod';
|
||||||
import type { RgbColor } from 'react-colorful';
|
|
||||||
|
|
||||||
export type DrawingTool = 'brush' | 'eraser';
|
const zTool = z.enum(['brush', 'eraser', 'move', 'rect']);
|
||||||
|
export type Tool = z.infer<typeof zTool>;
|
||||||
|
const zDrawingTool = zTool.extract(['brush', 'eraser']);
|
||||||
|
export type DrawingTool = z.infer<typeof zDrawingTool>;
|
||||||
|
|
||||||
export type Tool = DrawingTool | 'move' | 'rect';
|
const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, {
|
||||||
|
message: 'Must have an even number of points',
|
||||||
|
});
|
||||||
|
const zVectorMaskLine = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: z.literal('vector_mask_line'),
|
||||||
|
tool: zDrawingTool,
|
||||||
|
strokeWidth: z.number().min(1),
|
||||||
|
points: zPoints,
|
||||||
|
});
|
||||||
|
export type VectorMaskLine = z.infer<typeof zVectorMaskLine>;
|
||||||
|
|
||||||
export type VectorMaskLine = {
|
const zVectorMaskRect = z.object({
|
||||||
id: string;
|
id: z.string(),
|
||||||
type: 'vector_mask_line';
|
type: z.literal('vector_mask_rect'),
|
||||||
tool: DrawingTool;
|
x: z.number(),
|
||||||
strokeWidth: number;
|
y: z.number(),
|
||||||
points: number[];
|
width: z.number().min(1),
|
||||||
};
|
height: z.number().min(1),
|
||||||
|
});
|
||||||
|
export type VectorMaskRect = z.infer<typeof zVectorMaskRect>;
|
||||||
|
|
||||||
export type VectorMaskRect = {
|
const zLayerBase = z.object({
|
||||||
id: string;
|
id: z.string(),
|
||||||
type: 'vector_mask_rect';
|
isEnabled: z.boolean().default(true),
|
||||||
x: number;
|
isSelected: z.boolean().default(true),
|
||||||
y: number;
|
});
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LayerBase = {
|
const zRect = z.object({
|
||||||
id: string;
|
x: z.number(),
|
||||||
isEnabled: boolean;
|
y: z.number(),
|
||||||
};
|
width: z.number().min(1),
|
||||||
|
height: z.number().min(1),
|
||||||
|
});
|
||||||
|
const zRenderableLayerBase = zLayerBase.extend({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
bbox: zRect.nullable(),
|
||||||
|
bboxNeedsUpdate: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
type RenderableLayerBase = LayerBase & {
|
const zControlAdapterLayer = zRenderableLayerBase.extend({
|
||||||
x: number;
|
type: z.literal('control_adapter_layer'),
|
||||||
y: number;
|
opacity: z.number().gte(0).lte(1),
|
||||||
bbox: IRect | null;
|
isFilterEnabled: z.boolean(),
|
||||||
bboxNeedsUpdate: boolean;
|
controlAdapter: z.discriminatedUnion('type', [zControlNetConfigV2, zT2IAdapterConfigV2]),
|
||||||
isSelected: boolean;
|
});
|
||||||
};
|
export type ControlAdapterLayer = z.infer<typeof zControlAdapterLayer>;
|
||||||
|
|
||||||
export type ControlAdapterLayer = RenderableLayerBase & {
|
const zIPAdapterLayer = zLayerBase.extend({
|
||||||
type: 'control_adapter_layer'; // technically, also t2i adapter layer
|
type: z.literal('ip_adapter_layer'),
|
||||||
opacity: number;
|
ipAdapter: zIPAdapterConfigV2,
|
||||||
isFilterEnabled: boolean;
|
});
|
||||||
controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2;
|
export type IPAdapterLayer = z.infer<typeof zIPAdapterLayer>;
|
||||||
};
|
|
||||||
|
|
||||||
export type IPAdapterLayer = LayerBase & {
|
const zRgbColor = z.object({
|
||||||
type: 'ip_adapter_layer';
|
r: z.number().int().min(0).max(255),
|
||||||
ipAdapter: IPAdapterConfigV2;
|
g: z.number().int().min(0).max(255),
|
||||||
};
|
b: z.number().int().min(0).max(255),
|
||||||
|
});
|
||||||
|
const zRegionalGuidanceLayer = zRenderableLayerBase.extend({
|
||||||
|
type: z.literal('regional_guidance_layer'),
|
||||||
|
maskObjects: z.array(z.discriminatedUnion('type', [zVectorMaskLine, zVectorMaskRect])),
|
||||||
|
positivePrompt: zParameterPositivePrompt.nullable(),
|
||||||
|
negativePrompt: zParameterNegativePrompt.nullable(),
|
||||||
|
ipAdapters: z.array(zIPAdapterConfigV2),
|
||||||
|
previewColor: zRgbColor,
|
||||||
|
autoNegative: zAutoNegative,
|
||||||
|
uploadedMaskImage: zImageWithDims.nullable(),
|
||||||
|
});
|
||||||
|
export type RegionalGuidanceLayer = z.infer<typeof zRegionalGuidanceLayer>;
|
||||||
|
|
||||||
export type RegionalGuidanceLayer = RenderableLayerBase & {
|
const zInitialImageLayer = zRenderableLayerBase.extend({
|
||||||
type: 'regional_guidance_layer';
|
type: z.literal('initial_image_layer'),
|
||||||
maskObjects: (VectorMaskLine | VectorMaskRect)[];
|
opacity: z.number().gte(0).lte(1),
|
||||||
positivePrompt: ParameterPositivePrompt | null;
|
image: zImageWithDims.nullable(),
|
||||||
negativePrompt: ParameterNegativePrompt | null; // Up to one text prompt per mask
|
denoisingStrength: zParameterStrength,
|
||||||
ipAdapters: IPAdapterConfigV2[]; // Any number of image prompts
|
});
|
||||||
previewColor: RgbColor;
|
export type InitialImageLayer = z.infer<typeof zInitialImageLayer>;
|
||||||
autoNegative: ParameterAutoNegative;
|
|
||||||
needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object
|
|
||||||
uploadedMaskImage: ImageWithDims | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InitialImageLayer = RenderableLayerBase & {
|
export const zLayer = z.discriminatedUnion('type', [
|
||||||
type: 'initial_image_layer';
|
zRegionalGuidanceLayer,
|
||||||
opacity: number;
|
zControlAdapterLayer,
|
||||||
image: ImageWithDims | null;
|
zIPAdapterLayer,
|
||||||
};
|
zInitialImageLayer,
|
||||||
|
]);
|
||||||
export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer | InitialImageLayer;
|
export type Layer = z.infer<typeof zLayer>;
|
||||||
|
|
||||||
export type ControlLayersState = {
|
export type ControlLayersState = {
|
||||||
_version: 1;
|
_version: 3;
|
||||||
selectedLayerId: string | null;
|
selectedLayerId: string | null;
|
||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
brushSize: number;
|
brushSize: number;
|
||||||
|
@ -2,7 +2,6 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
|||||||
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||||
import { RG_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/store/controlLayersSlice';
|
import { RG_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { Layer as KonvaLayerType } from 'konva/lib/Layer';
|
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
@ -54,34 +53,30 @@ const getImageDataBbox = (imageData: ImageData): Extents | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers.
|
* Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer
|
||||||
* @param layer The konva layer to get the bounding box of.
|
* to be captured, manipulated or analyzed without interference from other layers.
|
||||||
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
|
* @param layer The konva layer to clone.
|
||||||
|
* @returns The cloned stage and layer.
|
||||||
*/
|
*/
|
||||||
export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = false): IRect | null => {
|
const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage; layerClone: Konva.Layer } => {
|
||||||
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
|
|
||||||
//
|
|
||||||
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
|
|
||||||
// by calculating the extents of individual shapes from their "vector" shape data.
|
|
||||||
//
|
|
||||||
// This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines.
|
|
||||||
// These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large.
|
|
||||||
const stage = layer.getStage();
|
const stage = layer.getStage();
|
||||||
|
|
||||||
// Construct and offscreen canvas on which we will do the bbox calculations.
|
// Construct an offscreen canvas with the same dimensions as the layer's stage.
|
||||||
const offscreenStageContainer = document.createElement('div');
|
const offscreenStageContainer = document.createElement('div');
|
||||||
const offscreenStage = new Konva.Stage({
|
const stageClone = new Konva.Stage({
|
||||||
container: offscreenStageContainer,
|
container: offscreenStageContainer,
|
||||||
|
x: stage.x(),
|
||||||
|
y: stage.y(),
|
||||||
width: stage.width(),
|
width: stage.width(),
|
||||||
height: stage.height(),
|
height: stage.height(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clone the layer and filter out unwanted children.
|
// Clone the layer and filter out unwanted children.
|
||||||
const layerClone = layer.clone();
|
const layerClone = layer.clone();
|
||||||
offscreenStage.add(layerClone);
|
stageClone.add(layerClone);
|
||||||
|
|
||||||
for (const child of layerClone.getChildren()) {
|
for (const child of layerClone.getChildren()) {
|
||||||
if (child.name() === RG_LAYER_OBJECT_GROUP_NAME) {
|
if (child.name() === RG_LAYER_OBJECT_GROUP_NAME && child.hasChildren()) {
|
||||||
// We need to cache the group to ensure it composites out eraser strokes correctly
|
// We need to cache the group to ensure it composites out eraser strokes correctly
|
||||||
child.opacity(1);
|
child.opacity(1);
|
||||||
child.cache();
|
child.cache();
|
||||||
@ -91,11 +86,31 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { stageClone, layerClone };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers.
|
||||||
|
* @param layer The konva layer to get the bounding box of.
|
||||||
|
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
|
||||||
|
*/
|
||||||
|
export const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect | null => {
|
||||||
|
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
|
||||||
|
//
|
||||||
|
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
|
||||||
|
// by calculating the extents of individual shapes from their "vector" shape data.
|
||||||
|
//
|
||||||
|
// This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines.
|
||||||
|
// These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large.
|
||||||
|
const { stageClone, layerClone } = getIsolatedRGLayerClone(layer);
|
||||||
|
|
||||||
// Get a worst-case rect using the relatively fast `getClientRect`.
|
// Get a worst-case rect using the relatively fast `getClientRect`.
|
||||||
const layerRect = layerClone.getClientRect();
|
const layerRect = layerClone.getClientRect();
|
||||||
|
if (layerRect.width === 0 || layerRect.height === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// Capture the image data with the above rect.
|
// Capture the image data with the above rect.
|
||||||
const layerImageData = offscreenStage
|
const layerImageData = stageClone
|
||||||
.toCanvas(layerRect)
|
.toCanvas(layerRect)
|
||||||
.getContext('2d')
|
.getContext('2d')
|
||||||
?.getImageData(0, 0, layerRect.width, layerRect.height);
|
?.getImageData(0, 0, layerRect.width, layerRect.height);
|
||||||
@ -114,8 +129,8 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal
|
|||||||
|
|
||||||
// Correct the bounding box to be relative to the layer's position.
|
// Correct the bounding box to be relative to the layer's position.
|
||||||
const correctedLayerBbox = {
|
const correctedLayerBbox = {
|
||||||
x: layerBbox.minX - Math.floor(stage.x()) + layerRect.x - Math.floor(layer.x()),
|
x: layerBbox.minX - Math.floor(stageClone.x()) + layerRect.x - Math.floor(layer.x()),
|
||||||
y: layerBbox.minY - Math.floor(stage.y()) + layerRect.y - Math.floor(layer.y()),
|
y: layerBbox.minY - Math.floor(stageClone.y()) + layerRect.y - Math.floor(layer.y()),
|
||||||
width: layerBbox.maxX - layerBbox.minX,
|
width: layerBbox.maxX - layerBbox.minX,
|
||||||
height: layerBbox.maxY - layerBbox.minY,
|
height: layerBbox.maxY - layerBbox.minY,
|
||||||
};
|
};
|
||||||
@ -123,7 +138,13 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal
|
|||||||
return correctedLayerBbox;
|
return correctedLayerBbox;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLayerBboxFast = (layer: KonvaLayerType): IRect => {
|
/**
|
||||||
|
* Get the bounding box of a konva layer. This function is faster than `getLayerBboxPixels` but less accurate. It
|
||||||
|
* should only be used when there are no eraser strokes or shapes in the layer.
|
||||||
|
* @param layer The konva layer to get the bounding box of.
|
||||||
|
* @returns The bounding box of the layer.
|
||||||
|
*/
|
||||||
|
export const getLayerBboxFast = (layer: Konva.Layer): IRect => {
|
||||||
const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG);
|
const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG);
|
||||||
return {
|
return {
|
||||||
x: Math.floor(bbox.x),
|
x: Math.floor(bbox.x),
|
||||||
|
@ -1,23 +1,93 @@
|
|||||||
import type { S } from 'services/api/types';
|
import type { Invocation } from 'services/api/types';
|
||||||
import type { Equals } from 'tsafe';
|
import type { Equals } from 'tsafe';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
import { describe, test } from 'vitest';
|
import { describe, test } from 'vitest';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
CannyProcessorConfig,
|
||||||
CLIPVisionModelV2,
|
CLIPVisionModelV2,
|
||||||
|
ColorMapProcessorConfig,
|
||||||
|
ContentShuffleProcessorConfig,
|
||||||
ControlModeV2,
|
ControlModeV2,
|
||||||
DepthAnythingModelSize,
|
DepthAnythingModelSize,
|
||||||
|
DepthAnythingProcessorConfig,
|
||||||
|
DWOpenposeProcessorConfig,
|
||||||
|
HedProcessorConfig,
|
||||||
IPMethodV2,
|
IPMethodV2,
|
||||||
|
LineartAnimeProcessorConfig,
|
||||||
|
LineartProcessorConfig,
|
||||||
|
MediapipeFaceProcessorConfig,
|
||||||
|
MidasDepthProcessorConfig,
|
||||||
|
MlsdProcessorConfig,
|
||||||
|
NormalbaeProcessorConfig,
|
||||||
|
PidiProcessorConfig,
|
||||||
ProcessorConfig,
|
ProcessorConfig,
|
||||||
ProcessorTypeV2,
|
ProcessorTypeV2,
|
||||||
|
ZoeDepthProcessorConfig,
|
||||||
} from './controlAdapters';
|
} from './controlAdapters';
|
||||||
|
|
||||||
describe('Control Adapter Types', () => {
|
describe('Control Adapter Types', () => {
|
||||||
test('ProcessorType', () => assert<Equals<ProcessorConfig['type'], ProcessorTypeV2>>());
|
test('ProcessorType', () => {
|
||||||
test('IP Adapter Method', () => assert<Equals<NonNullable<S['IPAdapterInvocation']['method']>, IPMethodV2>>());
|
assert<Equals<ProcessorConfig['type'], ProcessorTypeV2>>();
|
||||||
test('CLIP Vision Model', () =>
|
});
|
||||||
assert<Equals<NonNullable<S['IPAdapterInvocation']['clip_vision_model']>, CLIPVisionModelV2>>());
|
test('IP Adapter Method', () => {
|
||||||
test('Control Mode', () => assert<Equals<NonNullable<S['ControlNetInvocation']['control_mode']>, ControlModeV2>>());
|
assert<Equals<NonNullable<Invocation<'ip_adapter'>['method']>, IPMethodV2>>();
|
||||||
test('DepthAnything Model Size', () =>
|
});
|
||||||
assert<Equals<NonNullable<S['DepthAnythingImageProcessorInvocation']['model_size']>, DepthAnythingModelSize>>());
|
test('CLIP Vision Model', () => {
|
||||||
|
assert<Equals<NonNullable<Invocation<'ip_adapter'>['clip_vision_model']>, CLIPVisionModelV2>>();
|
||||||
|
});
|
||||||
|
test('Control Mode', () => {
|
||||||
|
assert<Equals<NonNullable<Invocation<'controlnet'>['control_mode']>, ControlModeV2>>();
|
||||||
|
});
|
||||||
|
test('DepthAnything Model Size', () => {
|
||||||
|
assert<Equals<NonNullable<Invocation<'depth_anything_image_processor'>['model_size']>, DepthAnythingModelSize>>();
|
||||||
|
});
|
||||||
|
test('Processor Configs', () => {
|
||||||
|
// The processor configs are manually modeled zod schemas. This test ensures that the inferred types are correct.
|
||||||
|
// The types prefixed with `_` are types generated from OpenAPI, while the types without the prefix are manually modeled.
|
||||||
|
assert<Equals<_CannyProcessorConfig, CannyProcessorConfig>>();
|
||||||
|
assert<Equals<_ColorMapProcessorConfig, ColorMapProcessorConfig>>();
|
||||||
|
assert<Equals<_ContentShuffleProcessorConfig, ContentShuffleProcessorConfig>>();
|
||||||
|
assert<Equals<_DepthAnythingProcessorConfig, DepthAnythingProcessorConfig>>();
|
||||||
|
assert<Equals<_HedProcessorConfig, HedProcessorConfig>>();
|
||||||
|
assert<Equals<_LineartAnimeProcessorConfig, LineartAnimeProcessorConfig>>();
|
||||||
|
assert<Equals<_LineartProcessorConfig, LineartProcessorConfig>>();
|
||||||
|
assert<Equals<_MediapipeFaceProcessorConfig, MediapipeFaceProcessorConfig>>();
|
||||||
|
assert<Equals<_MidasDepthProcessorConfig, MidasDepthProcessorConfig>>();
|
||||||
|
assert<Equals<_MlsdProcessorConfig, MlsdProcessorConfig>>();
|
||||||
|
assert<Equals<_NormalbaeProcessorConfig, NormalbaeProcessorConfig>>();
|
||||||
|
assert<Equals<_DWOpenposeProcessorConfig, DWOpenposeProcessorConfig>>();
|
||||||
|
assert<Equals<_PidiProcessorConfig, PidiProcessorConfig>>();
|
||||||
|
assert<Equals<_ZoeDepthProcessorConfig, ZoeDepthProcessorConfig>>();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Types derived from OpenAPI
|
||||||
|
type _CannyProcessorConfig = Required<
|
||||||
|
Pick<Invocation<'canny_image_processor'>, 'id' | 'type' | 'low_threshold' | 'high_threshold'>
|
||||||
|
>;
|
||||||
|
type _ColorMapProcessorConfig = Required<
|
||||||
|
Pick<Invocation<'color_map_image_processor'>, 'id' | 'type' | 'color_map_tile_size'>
|
||||||
|
>;
|
||||||
|
type _ContentShuffleProcessorConfig = Required<
|
||||||
|
Pick<Invocation<'content_shuffle_image_processor'>, 'id' | 'type' | 'w' | 'h' | 'f'>
|
||||||
|
>;
|
||||||
|
type _DepthAnythingProcessorConfig = Required<
|
||||||
|
Pick<Invocation<'depth_anything_image_processor'>, 'id' | 'type' | 'model_size'>
|
||||||
|
>;
|
||||||
|
type _HedProcessorConfig = Required<Pick<Invocation<'hed_image_processor'>, 'id' | 'type' | 'scribble'>>;
|
||||||
|
type _LineartAnimeProcessorConfig = Required<Pick<Invocation<'lineart_anime_image_processor'>, 'id' | 'type'>>;
|
||||||
|
type _LineartProcessorConfig = Required<Pick<Invocation<'lineart_image_processor'>, 'id' | 'type' | 'coarse'>>;
|
||||||
|
type _MediapipeFaceProcessorConfig = Required<
|
||||||
|
Pick<Invocation<'mediapipe_face_processor'>, 'id' | 'type' | 'max_faces' | 'min_confidence'>
|
||||||
|
>;
|
||||||
|
type _MidasDepthProcessorConfig = Required<
|
||||||
|
Pick<Invocation<'midas_depth_image_processor'>, 'id' | 'type' | 'a_mult' | 'bg_th'>
|
||||||
|
>;
|
||||||
|
type _MlsdProcessorConfig = Required<Pick<Invocation<'mlsd_image_processor'>, 'id' | 'type' | 'thr_v' | 'thr_d'>>;
|
||||||
|
type _NormalbaeProcessorConfig = Required<Pick<Invocation<'normalbae_image_processor'>, 'id' | 'type'>>;
|
||||||
|
type _DWOpenposeProcessorConfig = Required<
|
||||||
|
Pick<Invocation<'dw_openpose_image_processor'>, 'id' | 'type' | 'draw_body' | 'draw_face' | 'draw_hands'>
|
||||||
|
>;
|
||||||
|
type _PidiProcessorConfig = Required<Pick<Invocation<'pidi_image_processor'>, 'id' | 'type' | 'safe' | 'scribble'>>;
|
||||||
|
type _ZoeDepthProcessorConfig = Required<Pick<Invocation<'zoe_depth_image_processor'>, 'id' | 'type'>>;
|
||||||
|
@ -1,117 +1,176 @@
|
|||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import type {
|
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||||
ParameterControlNetModel,
|
|
||||||
ParameterIPAdapterModel,
|
|
||||||
ParameterT2IAdapterModel,
|
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
|
||||||
import { merge, omit } from 'lodash-es';
|
import { merge, omit } from 'lodash-es';
|
||||||
import type {
|
import type { BaseModelType, ControlNetModelConfig, Graph, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
|
||||||
BaseModelType,
|
|
||||||
CannyImageProcessorInvocation,
|
|
||||||
ColorMapImageProcessorInvocation,
|
|
||||||
ContentShuffleImageProcessorInvocation,
|
|
||||||
ControlNetModelConfig,
|
|
||||||
DepthAnythingImageProcessorInvocation,
|
|
||||||
DWOpenposeImageProcessorInvocation,
|
|
||||||
Graph,
|
|
||||||
HedImageProcessorInvocation,
|
|
||||||
ImageDTO,
|
|
||||||
LineartAnimeImageProcessorInvocation,
|
|
||||||
LineartImageProcessorInvocation,
|
|
||||||
MediapipeFaceProcessorInvocation,
|
|
||||||
MidasDepthImageProcessorInvocation,
|
|
||||||
MlsdImageProcessorInvocation,
|
|
||||||
NormalbaeImageProcessorInvocation,
|
|
||||||
PidiImageProcessorInvocation,
|
|
||||||
T2IAdapterModelConfig,
|
|
||||||
ZoeDepthImageProcessorInvocation,
|
|
||||||
} from 'services/api/types';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const zId = z.string().min(1);
|
||||||
|
|
||||||
|
const zCannyProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('canny_image_processor'),
|
||||||
|
low_threshold: z.number().int().gte(0).lte(255),
|
||||||
|
high_threshold: z.number().int().gte(0).lte(255),
|
||||||
|
});
|
||||||
|
export type CannyProcessorConfig = z.infer<typeof zCannyProcessorConfig>;
|
||||||
|
|
||||||
|
const zColorMapProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('color_map_image_processor'),
|
||||||
|
color_map_tile_size: z.number().int().gte(1),
|
||||||
|
});
|
||||||
|
export type ColorMapProcessorConfig = z.infer<typeof zColorMapProcessorConfig>;
|
||||||
|
|
||||||
|
const zContentShuffleProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('content_shuffle_image_processor'),
|
||||||
|
w: z.number().int().gte(0),
|
||||||
|
h: z.number().int().gte(0),
|
||||||
|
f: z.number().int().gte(0),
|
||||||
|
});
|
||||||
|
export type ContentShuffleProcessorConfig = z.infer<typeof zContentShuffleProcessorConfig>;
|
||||||
|
|
||||||
const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']);
|
const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']);
|
||||||
export type DepthAnythingModelSize = z.infer<typeof zDepthAnythingModelSize>;
|
export type DepthAnythingModelSize = z.infer<typeof zDepthAnythingModelSize>;
|
||||||
export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize =>
|
export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize =>
|
||||||
zDepthAnythingModelSize.safeParse(v).success;
|
zDepthAnythingModelSize.safeParse(v).success;
|
||||||
|
const zDepthAnythingProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('depth_anything_image_processor'),
|
||||||
|
model_size: zDepthAnythingModelSize,
|
||||||
|
});
|
||||||
|
export type DepthAnythingProcessorConfig = z.infer<typeof zDepthAnythingProcessorConfig>;
|
||||||
|
|
||||||
export type CannyProcessorConfig = Required<
|
const zHedProcessorConfig = z.object({
|
||||||
Pick<CannyImageProcessorInvocation, 'id' | 'type' | 'low_threshold' | 'high_threshold'>
|
id: zId,
|
||||||
>;
|
type: z.literal('hed_image_processor'),
|
||||||
export type ColorMapProcessorConfig = Required<
|
scribble: z.boolean(),
|
||||||
Pick<ColorMapImageProcessorInvocation, 'id' | 'type' | 'color_map_tile_size'>
|
});
|
||||||
>;
|
export type HedProcessorConfig = z.infer<typeof zHedProcessorConfig>;
|
||||||
export type ContentShuffleProcessorConfig = Required<
|
|
||||||
Pick<ContentShuffleImageProcessorInvocation, 'id' | 'type' | 'w' | 'h' | 'f'>
|
|
||||||
>;
|
|
||||||
export type DepthAnythingProcessorConfig = Required<
|
|
||||||
Pick<DepthAnythingImageProcessorInvocation, 'id' | 'type' | 'model_size'>
|
|
||||||
>;
|
|
||||||
export type HedProcessorConfig = Required<Pick<HedImageProcessorInvocation, 'id' | 'type' | 'scribble'>>;
|
|
||||||
type LineartAnimeProcessorConfig = Required<Pick<LineartAnimeImageProcessorInvocation, 'id' | 'type'>>;
|
|
||||||
export type LineartProcessorConfig = Required<Pick<LineartImageProcessorInvocation, 'id' | 'type' | 'coarse'>>;
|
|
||||||
export type MediapipeFaceProcessorConfig = Required<
|
|
||||||
Pick<MediapipeFaceProcessorInvocation, 'id' | 'type' | 'max_faces' | 'min_confidence'>
|
|
||||||
>;
|
|
||||||
export type MidasDepthProcessorConfig = Required<
|
|
||||||
Pick<MidasDepthImageProcessorInvocation, 'id' | 'type' | 'a_mult' | 'bg_th'>
|
|
||||||
>;
|
|
||||||
export type MlsdProcessorConfig = Required<Pick<MlsdImageProcessorInvocation, 'id' | 'type' | 'thr_v' | 'thr_d'>>;
|
|
||||||
type NormalbaeProcessorConfig = Required<Pick<NormalbaeImageProcessorInvocation, 'id' | 'type'>>;
|
|
||||||
export type DWOpenposeProcessorConfig = Required<
|
|
||||||
Pick<DWOpenposeImageProcessorInvocation, 'id' | 'type' | 'draw_body' | 'draw_face' | 'draw_hands'>
|
|
||||||
>;
|
|
||||||
export type PidiProcessorConfig = Required<Pick<PidiImageProcessorInvocation, 'id' | 'type' | 'safe' | 'scribble'>>;
|
|
||||||
type ZoeDepthProcessorConfig = Required<Pick<ZoeDepthImageProcessorInvocation, 'id' | 'type'>>;
|
|
||||||
|
|
||||||
export type ProcessorConfig =
|
const zLineartAnimeProcessorConfig = z.object({
|
||||||
| CannyProcessorConfig
|
id: zId,
|
||||||
| ColorMapProcessorConfig
|
type: z.literal('lineart_anime_image_processor'),
|
||||||
| ContentShuffleProcessorConfig
|
});
|
||||||
| DepthAnythingProcessorConfig
|
export type LineartAnimeProcessorConfig = z.infer<typeof zLineartAnimeProcessorConfig>;
|
||||||
| HedProcessorConfig
|
|
||||||
| LineartAnimeProcessorConfig
|
|
||||||
| LineartProcessorConfig
|
|
||||||
| MediapipeFaceProcessorConfig
|
|
||||||
| MidasDepthProcessorConfig
|
|
||||||
| MlsdProcessorConfig
|
|
||||||
| NormalbaeProcessorConfig
|
|
||||||
| DWOpenposeProcessorConfig
|
|
||||||
| PidiProcessorConfig
|
|
||||||
| ZoeDepthProcessorConfig;
|
|
||||||
|
|
||||||
export type ImageWithDims = {
|
const zLineartProcessorConfig = z.object({
|
||||||
imageName: string;
|
id: zId,
|
||||||
width: number;
|
type: z.literal('lineart_image_processor'),
|
||||||
height: number;
|
coarse: z.boolean(),
|
||||||
};
|
});
|
||||||
|
export type LineartProcessorConfig = z.infer<typeof zLineartProcessorConfig>;
|
||||||
|
|
||||||
type ControlAdapterBase = {
|
const zMediapipeFaceProcessorConfig = z.object({
|
||||||
id: string;
|
id: zId,
|
||||||
weight: number;
|
type: z.literal('mediapipe_face_processor'),
|
||||||
image: ImageWithDims | null;
|
max_faces: z.number().int().gte(1),
|
||||||
processedImage: ImageWithDims | null;
|
min_confidence: z.number().gte(0).lte(1),
|
||||||
isProcessingImage: boolean;
|
});
|
||||||
processorConfig: ProcessorConfig | null;
|
export type MediapipeFaceProcessorConfig = z.infer<typeof zMediapipeFaceProcessorConfig>;
|
||||||
beginEndStepPct: [number, number];
|
|
||||||
};
|
const zMidasDepthProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('midas_depth_image_processor'),
|
||||||
|
a_mult: z.number().gte(0),
|
||||||
|
bg_th: z.number().gte(0),
|
||||||
|
});
|
||||||
|
export type MidasDepthProcessorConfig = z.infer<typeof zMidasDepthProcessorConfig>;
|
||||||
|
|
||||||
|
const zMlsdProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('mlsd_image_processor'),
|
||||||
|
thr_v: z.number().gte(0),
|
||||||
|
thr_d: z.number().gte(0),
|
||||||
|
});
|
||||||
|
export type MlsdProcessorConfig = z.infer<typeof zMlsdProcessorConfig>;
|
||||||
|
|
||||||
|
const zNormalbaeProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('normalbae_image_processor'),
|
||||||
|
});
|
||||||
|
export type NormalbaeProcessorConfig = z.infer<typeof zNormalbaeProcessorConfig>;
|
||||||
|
|
||||||
|
const zDWOpenposeProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('dw_openpose_image_processor'),
|
||||||
|
draw_body: z.boolean(),
|
||||||
|
draw_face: z.boolean(),
|
||||||
|
draw_hands: z.boolean(),
|
||||||
|
});
|
||||||
|
export type DWOpenposeProcessorConfig = z.infer<typeof zDWOpenposeProcessorConfig>;
|
||||||
|
|
||||||
|
const zPidiProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('pidi_image_processor'),
|
||||||
|
safe: z.boolean(),
|
||||||
|
scribble: z.boolean(),
|
||||||
|
});
|
||||||
|
export type PidiProcessorConfig = z.infer<typeof zPidiProcessorConfig>;
|
||||||
|
|
||||||
|
const zZoeDepthProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('zoe_depth_image_processor'),
|
||||||
|
});
|
||||||
|
export type ZoeDepthProcessorConfig = z.infer<typeof zZoeDepthProcessorConfig>;
|
||||||
|
|
||||||
|
const zProcessorConfig = z.discriminatedUnion('type', [
|
||||||
|
zCannyProcessorConfig,
|
||||||
|
zColorMapProcessorConfig,
|
||||||
|
zContentShuffleProcessorConfig,
|
||||||
|
zDepthAnythingProcessorConfig,
|
||||||
|
zHedProcessorConfig,
|
||||||
|
zLineartAnimeProcessorConfig,
|
||||||
|
zLineartProcessorConfig,
|
||||||
|
zMediapipeFaceProcessorConfig,
|
||||||
|
zMidasDepthProcessorConfig,
|
||||||
|
zMlsdProcessorConfig,
|
||||||
|
zNormalbaeProcessorConfig,
|
||||||
|
zDWOpenposeProcessorConfig,
|
||||||
|
zPidiProcessorConfig,
|
||||||
|
zZoeDepthProcessorConfig,
|
||||||
|
]);
|
||||||
|
export type ProcessorConfig = z.infer<typeof zProcessorConfig>;
|
||||||
|
|
||||||
|
export const zImageWithDims = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
width: z.number().int().positive(),
|
||||||
|
height: z.number().int().positive(),
|
||||||
|
});
|
||||||
|
export type ImageWithDims = z.infer<typeof zImageWithDims>;
|
||||||
|
|
||||||
|
const zBeginEndStepPct = z
|
||||||
|
.tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)])
|
||||||
|
.refine(([begin, end]) => begin < end, {
|
||||||
|
message: 'Begin must be less than end',
|
||||||
|
});
|
||||||
|
|
||||||
|
const zControlAdapterBase = z.object({
|
||||||
|
id: zId,
|
||||||
|
weight: z.number().gte(0).lte(1),
|
||||||
|
image: zImageWithDims.nullable(),
|
||||||
|
processedImage: zImageWithDims.nullable(),
|
||||||
|
processorConfig: zProcessorConfig.nullable(),
|
||||||
|
processorPendingBatchId: z.string().nullable().default(null),
|
||||||
|
beginEndStepPct: zBeginEndStepPct,
|
||||||
|
});
|
||||||
|
|
||||||
const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
|
const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
|
||||||
export type ControlModeV2 = z.infer<typeof zControlModeV2>;
|
export type ControlModeV2 = z.infer<typeof zControlModeV2>;
|
||||||
export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success;
|
export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success;
|
||||||
|
|
||||||
export type ControlNetConfigV2 = ControlAdapterBase & {
|
export const zControlNetConfigV2 = zControlAdapterBase.extend({
|
||||||
type: 'controlnet';
|
type: z.literal('controlnet'),
|
||||||
model: ParameterControlNetModel | null;
|
model: zModelIdentifierField.nullable(),
|
||||||
controlMode: ControlModeV2;
|
controlMode: zControlModeV2,
|
||||||
};
|
});
|
||||||
export const isControlNetConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is ControlNetConfigV2 =>
|
export type ControlNetConfigV2 = z.infer<typeof zControlNetConfigV2>;
|
||||||
ca.type === 'controlnet';
|
|
||||||
|
|
||||||
export type T2IAdapterConfigV2 = ControlAdapterBase & {
|
export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({
|
||||||
type: 't2i_adapter';
|
type: z.literal('t2i_adapter'),
|
||||||
model: ParameterT2IAdapterModel | null;
|
model: zModelIdentifierField.nullable(),
|
||||||
};
|
});
|
||||||
export const isT2IAdapterConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is T2IAdapterConfigV2 =>
|
export type T2IAdapterConfigV2 = z.infer<typeof zT2IAdapterConfigV2>;
|
||||||
ca.type === 't2i_adapter';
|
|
||||||
|
|
||||||
const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']);
|
const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']);
|
||||||
export type CLIPVisionModelV2 = z.infer<typeof zCLIPVisionModelV2>;
|
export type CLIPVisionModelV2 = z.infer<typeof zCLIPVisionModelV2>;
|
||||||
@ -121,16 +180,17 @@ const zIPMethodV2 = z.enum(['full', 'style', 'composition']);
|
|||||||
export type IPMethodV2 = z.infer<typeof zIPMethodV2>;
|
export type IPMethodV2 = z.infer<typeof zIPMethodV2>;
|
||||||
export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success;
|
export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success;
|
||||||
|
|
||||||
export type IPAdapterConfigV2 = {
|
export const zIPAdapterConfigV2 = z.object({
|
||||||
id: string;
|
id: zId,
|
||||||
type: 'ip_adapter';
|
type: z.literal('ip_adapter'),
|
||||||
weight: number;
|
weight: z.number().gte(0).lte(1),
|
||||||
method: IPMethodV2;
|
method: zIPMethodV2,
|
||||||
image: ImageWithDims | null;
|
image: zImageWithDims.nullable(),
|
||||||
model: ParameterIPAdapterModel | null;
|
model: zModelIdentifierField.nullable(),
|
||||||
clipVisionModel: CLIPVisionModelV2;
|
clipVisionModel: zCLIPVisionModelV2,
|
||||||
beginEndStepPct: [number, number];
|
beginEndStepPct: zBeginEndStepPct,
|
||||||
};
|
});
|
||||||
|
export type IPAdapterConfigV2 = z.infer<typeof zIPAdapterConfigV2>;
|
||||||
|
|
||||||
const zProcessorTypeV2 = z.enum([
|
const zProcessorTypeV2 = z.enum([
|
||||||
'canny_image_processor',
|
'canny_image_processor',
|
||||||
@ -190,7 +250,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
type: 'canny_image_processor',
|
type: 'canny_image_processor',
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@ -207,7 +267,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
type: 'color_map_image_processor',
|
type: 'color_map_image_processor',
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
content_shuffle_image_processor: {
|
content_shuffle_image_processor: {
|
||||||
@ -223,7 +283,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@ -239,7 +299,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
resolution: minDim(image),
|
resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -254,7 +314,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@ -269,7 +329,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@ -285,7 +345,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@ -302,7 +362,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@ -319,7 +379,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@ -336,7 +396,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@ -351,7 +411,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@ -369,7 +429,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -385,7 +445,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@ -400,7 +460,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -413,8 +473,8 @@ export const initialControlNetV2: Omit<ControlNetConfigV2, 'id'> = {
|
|||||||
controlMode: 'balanced',
|
controlMode: 'balanced',
|
||||||
image: null,
|
image: null,
|
||||||
processedImage: null,
|
processedImage: null,
|
||||||
isProcessingImage: false,
|
|
||||||
processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
|
processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
|
||||||
|
processorPendingBatchId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialT2IAdapterV2: Omit<T2IAdapterConfigV2, 'id'> = {
|
export const initialT2IAdapterV2: Omit<T2IAdapterConfigV2, 'id'> = {
|
||||||
@ -424,8 +484,8 @@ export const initialT2IAdapterV2: Omit<T2IAdapterConfigV2, 'id'> = {
|
|||||||
beginEndStepPct: [0, 1],
|
beginEndStepPct: [0, 1],
|
||||||
image: null,
|
image: null,
|
||||||
processedImage: null,
|
processedImage: null,
|
||||||
isProcessingImage: false,
|
|
||||||
processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
|
processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
|
||||||
|
processorPendingBatchId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialIPAdapterV2: Omit<IPAdapterConfigV2, 'id'> = {
|
export const initialIPAdapterV2: Omit<IPAdapterConfigV2, 'id'> = {
|
||||||
@ -462,7 +522,7 @@ export const buildControlAdapterProcessorV2 = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
|
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
|
||||||
imageName: image_name,
|
name: image_name,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
|
@ -437,8 +437,8 @@ const renderRegionalGuidanceLayer = (
|
|||||||
konvaObjectGroup.opacity(1);
|
konvaObjectGroup.opacity(1);
|
||||||
|
|
||||||
compositingRect.setAttrs({
|
compositingRect.setAttrs({
|
||||||
// The rect should be the size of the layer - use the fast method bc it's OK if the rect is larger
|
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
|
||||||
...getLayerBboxFast(konvaLayer),
|
...(!reduxLayer.bboxNeedsUpdate && reduxLayer.bbox ? reduxLayer.bbox : getLayerBboxFast(konvaLayer)),
|
||||||
fill: rgbColor,
|
fill: rgbColor,
|
||||||
opacity: globalMaskLayerOpacity,
|
opacity: globalMaskLayerOpacity,
|
||||||
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
||||||
@ -464,6 +464,7 @@ const createInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLay
|
|||||||
id: reduxLayer.id,
|
id: reduxLayer.id,
|
||||||
name: INITIAL_IMAGE_LAYER_NAME,
|
name: INITIAL_IMAGE_LAYER_NAME,
|
||||||
imageSmoothingEnabled: true,
|
imageSmoothingEnabled: true,
|
||||||
|
listening: false,
|
||||||
});
|
});
|
||||||
stage.add(konvaLayer);
|
stage.add(konvaLayer);
|
||||||
return konvaLayer;
|
return konvaLayer;
|
||||||
@ -483,6 +484,9 @@ const updateInitialImageLayerImageAttrs = (
|
|||||||
konvaImage: Konva.Image,
|
konvaImage: Konva.Image,
|
||||||
reduxLayer: InitialImageLayer
|
reduxLayer: InitialImageLayer
|
||||||
) => {
|
) => {
|
||||||
|
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching,
|
||||||
|
// but it doesn't seem to break anything.
|
||||||
|
// TODO(psyche): Investigate and report upstream.
|
||||||
const newWidth = stage.width() / stage.scaleX();
|
const newWidth = stage.width() / stage.scaleX();
|
||||||
const newHeight = stage.height() / stage.scaleY();
|
const newHeight = stage.height() / stage.scaleY();
|
||||||
if (
|
if (
|
||||||
@ -510,7 +514,7 @@ const updateInitialImageLayerImageSource = async (
|
|||||||
reduxLayer: InitialImageLayer
|
reduxLayer: InitialImageLayer
|
||||||
) => {
|
) => {
|
||||||
if (reduxLayer.image) {
|
if (reduxLayer.image) {
|
||||||
const { imageName } = reduxLayer.image;
|
const imageName = reduxLayer.image.name;
|
||||||
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
|
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
|
||||||
const imageDTO = await req.unwrap();
|
const imageDTO = await req.unwrap();
|
||||||
req.unsubscribe();
|
req.unsubscribe();
|
||||||
@ -543,7 +547,7 @@ const renderInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLay
|
|||||||
let imageSourceNeedsUpdate = false;
|
let imageSourceNeedsUpdate = false;
|
||||||
if (canvasImageSource instanceof HTMLImageElement) {
|
if (canvasImageSource instanceof HTMLImageElement) {
|
||||||
const image = reduxLayer.image;
|
const image = reduxLayer.image;
|
||||||
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) {
|
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.name)) {
|
||||||
imageSourceNeedsUpdate = true;
|
imageSourceNeedsUpdate = true;
|
||||||
} else if (!image) {
|
} else if (!image) {
|
||||||
imageSourceNeedsUpdate = true;
|
imageSourceNeedsUpdate = true;
|
||||||
@ -564,6 +568,7 @@ const createControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay
|
|||||||
id: reduxLayer.id,
|
id: reduxLayer.id,
|
||||||
name: CA_LAYER_NAME,
|
name: CA_LAYER_NAME,
|
||||||
imageSmoothingEnabled: true,
|
imageSmoothingEnabled: true,
|
||||||
|
listening: false,
|
||||||
});
|
});
|
||||||
stage.add(konvaLayer);
|
stage.add(konvaLayer);
|
||||||
return konvaLayer;
|
return konvaLayer;
|
||||||
@ -585,7 +590,7 @@ const updateControlNetLayerImageSource = async (
|
|||||||
) => {
|
) => {
|
||||||
const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image;
|
const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image;
|
||||||
if (image) {
|
if (image) {
|
||||||
const { imageName } = image;
|
const imageName = image.name;
|
||||||
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
|
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
|
||||||
const imageDTO = await req.unwrap();
|
const imageDTO = await req.unwrap();
|
||||||
req.unsubscribe();
|
req.unsubscribe();
|
||||||
@ -618,6 +623,9 @@ const updateControlNetLayerImageAttrs = (
|
|||||||
reduxLayer: ControlAdapterLayer
|
reduxLayer: ControlAdapterLayer
|
||||||
) => {
|
) => {
|
||||||
let needsCache = false;
|
let needsCache = false;
|
||||||
|
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching,
|
||||||
|
// but it doesn't seem to break anything.
|
||||||
|
// TODO(psyche): Investigate and report upstream.
|
||||||
const newWidth = stage.width() / stage.scaleX();
|
const newWidth = stage.width() / stage.scaleX();
|
||||||
const newHeight = stage.height() / stage.scaleY();
|
const newHeight = stage.height() / stage.scaleY();
|
||||||
const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0;
|
const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0;
|
||||||
@ -653,7 +661,7 @@ const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay
|
|||||||
let imageSourceNeedsUpdate = false;
|
let imageSourceNeedsUpdate = false;
|
||||||
if (canvasImageSource instanceof HTMLImageElement) {
|
if (canvasImageSource instanceof HTMLImageElement) {
|
||||||
const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image;
|
const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image;
|
||||||
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) {
|
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.name)) {
|
||||||
imageSourceNeedsUpdate = true;
|
imageSourceNeedsUpdate = true;
|
||||||
} else if (!image) {
|
} else if (!image) {
|
||||||
imageSourceNeedsUpdate = true;
|
imageSourceNeedsUpdate = true;
|
||||||
@ -702,6 +710,7 @@ const renderLayers = (
|
|||||||
if (isInitialImageLayer(reduxLayer)) {
|
if (isInitialImageLayer(reduxLayer)) {
|
||||||
renderInitialImageLayer(stage, reduxLayer);
|
renderInitialImageLayer(stage, reduxLayer);
|
||||||
}
|
}
|
||||||
|
// IP Adapter layers are not rendered
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -716,6 +725,7 @@ const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer) => {
|
|||||||
id: getLayerBboxId(reduxLayer.id),
|
id: getLayerBboxId(reduxLayer.id),
|
||||||
name: LAYER_BBOX_NAME,
|
name: LAYER_BBOX_NAME,
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
|
visible: false,
|
||||||
});
|
});
|
||||||
konvaLayer.add(rect);
|
konvaLayer.add(rect);
|
||||||
return rect;
|
return rect;
|
||||||
@ -725,18 +735,10 @@ const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer) => {
|
|||||||
* Renders the bounding boxes for the layers.
|
* Renders the bounding boxes for the layers.
|
||||||
* @param stage The konva stage to render on
|
* @param stage The konva stage to render on
|
||||||
* @param reduxLayers An array of all redux layers to draw bboxes for
|
* @param reduxLayers An array of all redux layers to draw bboxes for
|
||||||
* @param selectedLayerId The selected layer's id
|
|
||||||
* @param tool The current tool
|
* @param tool The current tool
|
||||||
* @param onBboxChanged Callback for when the bbox is changed
|
|
||||||
* @param onBboxMouseDown Callback for when the bbox is clicked
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const renderBbox = (
|
const renderBboxes = (stage: Konva.Stage, reduxLayers: Layer[], tool: Tool) => {
|
||||||
stage: Konva.Stage,
|
|
||||||
reduxLayers: Layer[],
|
|
||||||
tool: Tool,
|
|
||||||
onBboxChanged: (layerId: string, bbox: IRect | null) => void
|
|
||||||
) => {
|
|
||||||
// Hide all bboxes so they don't interfere with getClientRect
|
// Hide all bboxes so they don't interfere with getClientRect
|
||||||
for (const bboxRect of stage.find<Konva.Rect>(`.${LAYER_BBOX_NAME}`)) {
|
for (const bboxRect of stage.find<Konva.Rect>(`.${LAYER_BBOX_NAME}`)) {
|
||||||
bboxRect.visible(false);
|
bboxRect.visible(false);
|
||||||
@ -747,36 +749,59 @@ const renderBbox = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const reduxLayer of reduxLayers) {
|
for (const reduxLayer of reduxLayers.filter(isRegionalGuidanceLayer)) {
|
||||||
if (reduxLayer.type === 'regional_guidance_layer') {
|
if (!reduxLayer.bbox) {
|
||||||
const konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`);
|
continue;
|
||||||
assert(konvaLayer, `Layer ${reduxLayer.id} not found in stage`);
|
}
|
||||||
|
const konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`);
|
||||||
|
assert(konvaLayer, `Layer ${reduxLayer.id} not found in stage`);
|
||||||
|
|
||||||
let bbox = reduxLayer.bbox;
|
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(reduxLayer, konvaLayer);
|
||||||
|
|
||||||
// We only need to recalculate the bbox if the layer has changed and it has objects
|
bboxRect.setAttrs({
|
||||||
if (reduxLayer.bboxNeedsUpdate && reduxLayer.maskObjects.length) {
|
visible: !reduxLayer.bboxNeedsUpdate,
|
||||||
// We only need to use the pixel-perfect bounding box if the layer has eraser strokes
|
listening: reduxLayer.isSelected,
|
||||||
bbox = reduxLayer.needsPixelBbox ? getLayerBboxPixels(konvaLayer) : getLayerBboxFast(konvaLayer);
|
x: reduxLayer.bbox.x,
|
||||||
// Update the layer's bbox in the redux store
|
y: reduxLayer.bbox.y,
|
||||||
onBboxChanged(reduxLayer.id, bbox);
|
width: reduxLayer.bbox.width,
|
||||||
|
height: reduxLayer.bbox.height,
|
||||||
|
stroke: reduxLayer.isSelected ? BBOX_SELECTED_STROKE : '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed.
|
||||||
|
* @param stage The konva stage to render on.
|
||||||
|
* @param reduxLayers An array of redux layers to calculate bboxes for
|
||||||
|
* @param onBboxChanged Callback for when the bounding box changes
|
||||||
|
*/
|
||||||
|
const updateBboxes = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
reduxLayers: Layer[],
|
||||||
|
onBboxChanged: (layerId: string, bbox: IRect | null) => void
|
||||||
|
) => {
|
||||||
|
for (const rgLayer of reduxLayers.filter(isRegionalGuidanceLayer)) {
|
||||||
|
const konvaLayer = stage.findOne<Konva.Layer>(`#${rgLayer.id}`);
|
||||||
|
assert(konvaLayer, `Layer ${rgLayer.id} not found in stage`);
|
||||||
|
// We only need to recalculate the bbox if the layer has changed
|
||||||
|
if (rgLayer.bboxNeedsUpdate) {
|
||||||
|
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rgLayer, konvaLayer);
|
||||||
|
|
||||||
|
// Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation
|
||||||
|
const visible = bboxRect.visible();
|
||||||
|
bboxRect.visible(false);
|
||||||
|
|
||||||
|
if (rgLayer.maskObjects.length === 0) {
|
||||||
|
// No objects - no bbox to calculate
|
||||||
|
onBboxChanged(rgLayer.id, null);
|
||||||
|
} else {
|
||||||
|
// Calculate the bbox by rendering the layer and checking its pixels
|
||||||
|
onBboxChanged(rgLayer.id, getLayerBboxPixels(konvaLayer));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bbox) {
|
// Restore the visibility of the bbox
|
||||||
continue;
|
bboxRect.visible(visible);
|
||||||
}
|
|
||||||
|
|
||||||
const rect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(reduxLayer, konvaLayer);
|
|
||||||
|
|
||||||
rect.setAttrs({
|
|
||||||
visible: true,
|
|
||||||
listening: reduxLayer.isSelected,
|
|
||||||
x: bbox.x,
|
|
||||||
y: bbox.y,
|
|
||||||
width: bbox.width,
|
|
||||||
height: bbox.height,
|
|
||||||
stroke: reduxLayer.isSelected ? BBOX_SELECTED_STROKE : '',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -893,10 +918,11 @@ const renderNoLayersMessage = (stage: Konva.Stage, layerCount: number, width: nu
|
|||||||
export const renderers = {
|
export const renderers = {
|
||||||
renderToolPreview,
|
renderToolPreview,
|
||||||
renderLayers,
|
renderLayers,
|
||||||
renderBbox,
|
renderBboxes,
|
||||||
renderBackground,
|
renderBackground,
|
||||||
renderNoLayersMessage,
|
renderNoLayersMessage,
|
||||||
arrangeLayers,
|
arrangeLayers,
|
||||||
|
updateBboxes,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEBOUNCE_MS = 300;
|
const DEBOUNCE_MS = 300;
|
||||||
@ -904,10 +930,11 @@ const DEBOUNCE_MS = 300;
|
|||||||
export const debouncedRenderers = {
|
export const debouncedRenderers = {
|
||||||
renderToolPreview: debounce(renderToolPreview, DEBOUNCE_MS),
|
renderToolPreview: debounce(renderToolPreview, DEBOUNCE_MS),
|
||||||
renderLayers: debounce(renderLayers, DEBOUNCE_MS),
|
renderLayers: debounce(renderLayers, DEBOUNCE_MS),
|
||||||
renderBbox: debounce(renderBbox, DEBOUNCE_MS),
|
renderBboxes: debounce(renderBboxes, DEBOUNCE_MS),
|
||||||
renderBackground: debounce(renderBackground, DEBOUNCE_MS),
|
renderBackground: debounce(renderBackground, DEBOUNCE_MS),
|
||||||
renderNoLayersMessage: debounce(renderNoLayersMessage, DEBOUNCE_MS),
|
renderNoLayersMessage: debounce(renderNoLayersMessage, DEBOUNCE_MS),
|
||||||
arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS),
|
arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS),
|
||||||
|
updateBboxes: debounce(updateBboxes, DEBOUNCE_MS),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,18 +46,16 @@ export const getImageUsage = (
|
|||||||
|
|
||||||
const isControlLayerImage = controlLayers.layers.some((l) => {
|
const isControlLayerImage = controlLayers.layers.some((l) => {
|
||||||
if (isRegionalGuidanceLayer(l)) {
|
if (isRegionalGuidanceLayer(l)) {
|
||||||
return l.ipAdapters.some((ipa) => ipa.image?.imageName === image_name);
|
return l.ipAdapters.some((ipa) => ipa.image?.name === image_name);
|
||||||
}
|
}
|
||||||
if (isControlAdapterLayer(l)) {
|
if (isControlAdapterLayer(l)) {
|
||||||
return (
|
return l.controlAdapter.image?.name === image_name || l.controlAdapter.processedImage?.name === image_name;
|
||||||
l.controlAdapter.image?.imageName === image_name || l.controlAdapter.processedImage?.imageName === image_name
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (isIPAdapterLayer(l)) {
|
if (isIPAdapterLayer(l)) {
|
||||||
return l.ipAdapter.image?.imageName === image_name;
|
return l.ipAdapter.image?.name === image_name;
|
||||||
}
|
}
|
||||||
if (isInitialImageLayer(l)) {
|
if (isInitialImageLayer(l)) {
|
||||||
return l.image?.imageName === image_name;
|
return l.image?.name === image_name;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
@ -1,23 +1,21 @@
|
|||||||
import type { Modifier } from '@dnd-kit/core';
|
import type { Modifier } from '@dnd-kit/core';
|
||||||
import { getEventCoordinates } from '@dnd-kit/utilities';
|
import { getEventCoordinates } from '@dnd-kit/utilities';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { $viewport } from 'features/nodes/store/nodesSlice';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
const selectZoom = createSelector([selectNodesSlice, activeTabNameSelector], (nodes, activeTabName) =>
|
|
||||||
activeTabName === 'workflows' ? nodes.viewport.zoom : 1
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies scaling to the drag transform (if on node editor tab) and centers it on cursor.
|
* Applies scaling to the drag transform (if on node editor tab) and centers it on cursor.
|
||||||
*/
|
*/
|
||||||
export const useScaledModifer = () => {
|
export const useScaledModifer = () => {
|
||||||
const zoom = useAppSelector(selectZoom);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
const workflowsViewport = useStore($viewport);
|
||||||
const modifier: Modifier = useCallback(
|
const modifier: Modifier = useCallback(
|
||||||
({ activatorEvent, draggingNodeRect, transform }) => {
|
({ activatorEvent, draggingNodeRect, transform }) => {
|
||||||
if (draggingNodeRect && activatorEvent) {
|
if (draggingNodeRect && activatorEvent) {
|
||||||
|
const zoom = activeTabName === 'workflows' ? workflowsViewport.zoom : 1;
|
||||||
const activatorCoordinates = getEventCoordinates(activatorEvent);
|
const activatorCoordinates = getEventCoordinates(activatorEvent);
|
||||||
|
|
||||||
if (!activatorCoordinates) {
|
if (!activatorCoordinates) {
|
||||||
@ -42,7 +40,7 @@ export const useScaledModifer = () => {
|
|||||||
|
|
||||||
return transform;
|
return transform;
|
||||||
},
|
},
|
||||||
[zoom]
|
[activeTabName, workflowsViewport.zoom]
|
||||||
);
|
);
|
||||||
|
|
||||||
return modifier;
|
return modifier;
|
||||||
|
@ -73,6 +73,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
const handleSendToImageToImage = useCallback(() => {
|
const handleSendToImageToImage = useCallback(() => {
|
||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(iiLayerAdded(imageDTO));
|
dispatch(iiLayerAdded(imageDTO));
|
||||||
|
dispatch(setActiveTab('generation'));
|
||||||
}, [dispatch, imageDTO]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
const handleSendToCanvas = useCallback(() => {
|
const handleSendToCanvas = useCallback(() => {
|
||||||
|
@ -11,6 +11,7 @@ import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggab
|
|||||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||||
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
||||||
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
||||||
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -102,6 +103,10 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onDoubleClick = useCallback(() => {
|
||||||
|
dispatch(isImageViewerOpenChanged(true));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleMouseOut = useCallback(() => {
|
const handleMouseOut = useCallback(() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
}, []);
|
}, []);
|
||||||
@ -143,6 +148,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
>
|
>
|
||||||
<IAIDndImage
|
<IAIDndImage
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
imageDTO={imageDTO}
|
imageDTO={imageDTO}
|
||||||
draggableData={draggableData}
|
draggableData={draggableData}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Box, Flex, IconButton, Tooltip, useShiftModifier } from '@invoke-ai/ui-library';
|
import { Box, Flex, IconButton, Tooltip, useShiftModifier } from '@invoke-ai/ui-library';
|
||||||
import { getOverlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
import { getOverlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
|
import { Formatter } from 'fracturedjsonjs';
|
||||||
import { isString } from 'lodash-es';
|
import { isString } from 'lodash-es';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
@ -7,6 +8,8 @@ import { memo, useCallback, useMemo } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCopyBold, PiDownloadSimpleBold } from 'react-icons/pi';
|
import { PiCopyBold, PiDownloadSimpleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const formatter = new Formatter();
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string;
|
label: string;
|
||||||
data: unknown;
|
data: unknown;
|
||||||
@ -20,7 +23,7 @@ const overlayscrollbarsOptions = getOverlayScrollbarsParams('scroll', 'scroll').
|
|||||||
|
|
||||||
const DataViewer = (props: Props) => {
|
const DataViewer = (props: Props) => {
|
||||||
const { label, data, fileName, withDownload = true, withCopy = true, extraCopyActions } = props;
|
const { label, data, fileName, withDownload = true, withCopy = true, extraCopyActions } = props;
|
||||||
const dataString = useMemo(() => (isString(data) ? data : JSON.stringify(data, null, 2)), [data]);
|
const dataString = useMemo(() => (isString(data) ? data : formatter.Serialize(data)) ?? '', [data]);
|
||||||
const shift = useShiftModifier();
|
const shift = useShiftModifier();
|
||||||
const handleCopy = useCallback(() => {
|
const handleCopy = useCallback(() => {
|
||||||
navigator.clipboard.writeText(dataString);
|
navigator.clipboard.writeText(dataString);
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets';
|
import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets';
|
||||||
import { MetadataControlNetsV2 } from 'features/metadata/components/MetadataControlNetsV2';
|
|
||||||
import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters';
|
import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters';
|
||||||
import { MetadataIPAdaptersV2 } from 'features/metadata/components/MetadataIPAdaptersV2';
|
|
||||||
import { MetadataItem } from 'features/metadata/components/MetadataItem';
|
import { MetadataItem } from 'features/metadata/components/MetadataItem';
|
||||||
|
import { MetadataLayers } from 'features/metadata/components/MetadataLayers';
|
||||||
import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs';
|
import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs';
|
||||||
import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters';
|
import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters';
|
||||||
import { MetadataT2IAdaptersV2 } from 'features/metadata/components/MetadataT2IAdaptersV2';
|
|
||||||
import { handlers } from 'features/metadata/util/handlers';
|
import { handlers } from 'features/metadata/util/handlers';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@ -39,8 +37,7 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
<MetadataItem metadata={metadata} handlers={handlers.scheduler} />
|
<MetadataItem metadata={metadata} handlers={handlers.scheduler} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.cfgScale} />
|
<MetadataItem metadata={metadata} handlers={handlers.cfgScale} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.cfgRescaleMultiplier} />
|
<MetadataItem metadata={metadata} handlers={handlers.cfgRescaleMultiplier} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.initialImage} />
|
{activeTabName !== 'generation' && <MetadataItem metadata={metadata} handlers={handlers.strength} />}
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.strength} />
|
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.hrfEnabled} />
|
<MetadataItem metadata={metadata} handlers={handlers.hrfEnabled} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.hrfMethod} />
|
<MetadataItem metadata={metadata} handlers={handlers.hrfMethod} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.hrfStrength} />
|
<MetadataItem metadata={metadata} handlers={handlers.hrfStrength} />
|
||||||
@ -52,12 +49,10 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
<MetadataItem metadata={metadata} handlers={handlers.refinerStart} />
|
<MetadataItem metadata={metadata} handlers={handlers.refinerStart} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.refinerSteps} />
|
<MetadataItem metadata={metadata} handlers={handlers.refinerSteps} />
|
||||||
<MetadataLoRAs metadata={metadata} />
|
<MetadataLoRAs metadata={metadata} />
|
||||||
|
{activeTabName === 'generation' && <MetadataLayers metadata={metadata} />}
|
||||||
{activeTabName !== 'generation' && <MetadataControlNets metadata={metadata} />}
|
{activeTabName !== 'generation' && <MetadataControlNets metadata={metadata} />}
|
||||||
{activeTabName !== 'generation' && <MetadataT2IAdapters metadata={metadata} />}
|
{activeTabName !== 'generation' && <MetadataT2IAdapters metadata={metadata} />}
|
||||||
{activeTabName !== 'generation' && <MetadataIPAdapters metadata={metadata} />}
|
{activeTabName !== 'generation' && <MetadataIPAdapters metadata={metadata} />}
|
||||||
{activeTabName === 'generation' && <MetadataControlNetsV2 metadata={metadata} />}
|
|
||||||
{activeTabName === 'generation' && <MetadataT2IAdaptersV2 metadata={metadata} />}
|
|
||||||
{activeTabName === 'generation' && <MetadataIPAdaptersV2 metadata={metadata} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDebouncedImageWorkflow } from 'services/api/hooks/useDebouncedImageWorkflow';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
import DataViewer from './DataViewer';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
image: ImageDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageMetadataGraphTabContent = ({ image }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentData } = useDebouncedImageWorkflow(image);
|
||||||
|
const graph = useMemo(() => {
|
||||||
|
if (currentData?.graph) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(currentData.graph);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [currentData]);
|
||||||
|
|
||||||
|
if (!graph) {
|
||||||
|
return <IAINoContentFallback label={t('nodes.noGraph')} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DataViewer data={graph} label={t('nodes.graph')} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ImageMetadataGraphTabContent);
|
@ -1,6 +1,7 @@
|
|||||||
import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library';
|
import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
|
import ImageMetadataGraphTabContent from 'features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent';
|
||||||
import { useMetadataItem } from 'features/metadata/hooks/useMetadataItem';
|
import { useMetadataItem } from 'features/metadata/hooks/useMetadataItem';
|
||||||
import { handlers } from 'features/metadata/util/handlers';
|
import { handlers } from 'features/metadata/util/handlers';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@ -52,6 +53,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
|||||||
<Tab>{t('metadata.metadata')}</Tab>
|
<Tab>{t('metadata.metadata')}</Tab>
|
||||||
<Tab>{t('metadata.imageDetails')}</Tab>
|
<Tab>{t('metadata.imageDetails')}</Tab>
|
||||||
<Tab>{t('metadata.workflow')}</Tab>
|
<Tab>{t('metadata.workflow')}</Tab>
|
||||||
|
<Tab>{t('nodes.graph')}</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
@ -81,6 +83,9 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
|||||||
<TabPanel>
|
<TabPanel>
|
||||||
<ImageMetadataWorkflowTabContent image={image} />
|
<ImageMetadataWorkflowTabContent image={image} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<ImageMetadataGraphTabContent image={image} />
|
||||||
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import { memo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDebouncedImageWorkflow } from 'services/api/hooks/useDebouncedImageWorkflow';
|
import { useDebouncedImageWorkflow } from 'services/api/hooks/useDebouncedImageWorkflow';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
@ -12,7 +12,17 @@ type Props = {
|
|||||||
|
|
||||||
const ImageMetadataWorkflowTabContent = ({ image }: Props) => {
|
const ImageMetadataWorkflowTabContent = ({ image }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { workflow } = useDebouncedImageWorkflow(image);
|
const { currentData } = useDebouncedImageWorkflow(image);
|
||||||
|
const workflow = useMemo(() => {
|
||||||
|
if (currentData?.workflow) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(currentData.workflow);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [currentData]);
|
||||||
|
|
||||||
if (!workflow) {
|
if (!workflow) {
|
||||||
return <IAINoContentFallback label={t('nodes.noWorkflow')} />;
|
return <IAINoContentFallback label={t('nodes.noWorkflow')} />;
|
||||||
|
@ -16,6 +16,7 @@ import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUps
|
|||||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||||
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
@ -84,6 +85,7 @@ const CurrentImageButtons = () => {
|
|||||||
}
|
}
|
||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(iiLayerAdded(imageDTO));
|
dispatch(iiLayerAdded(imageDTO));
|
||||||
|
dispatch(setActiveTab('generation'));
|
||||||
}, [dispatch, imageDTO]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
||||||
|
@ -22,7 +22,21 @@ const selectLastSelectedImageName = createSelector(
|
|||||||
(lastSelectedImage) => lastSelectedImage?.image_name
|
(lastSelectedImage) => lastSelectedImage?.image_name
|
||||||
);
|
);
|
||||||
|
|
||||||
const CurrentImagePreview = () => {
|
type Props = {
|
||||||
|
isDragDisabled?: boolean;
|
||||||
|
isDropDisabled?: boolean;
|
||||||
|
withNextPrevButtons?: boolean;
|
||||||
|
withMetadata?: boolean;
|
||||||
|
alwaysShowProgress?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CurrentImagePreview = ({
|
||||||
|
isDragDisabled = false,
|
||||||
|
isDropDisabled = false,
|
||||||
|
withNextPrevButtons = true,
|
||||||
|
withMetadata = true,
|
||||||
|
alwaysShowProgress = false,
|
||||||
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
||||||
const imageName = useAppSelector(selectLastSelectedImageName);
|
const imageName = useAppSelector(selectLastSelectedImageName);
|
||||||
@ -72,13 +86,15 @@ const CurrentImagePreview = () => {
|
|||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
position="relative"
|
position="relative"
|
||||||
>
|
>
|
||||||
{hasDenoiseProgress && shouldShowProgressInViewer ? (
|
{hasDenoiseProgress && (shouldShowProgressInViewer || alwaysShowProgress) ? (
|
||||||
<ProgressImage />
|
<ProgressImage />
|
||||||
) : (
|
) : (
|
||||||
<IAIDndImage
|
<IAIDndImage
|
||||||
imageDTO={imageDTO}
|
imageDTO={imageDTO}
|
||||||
droppableData={droppableData}
|
droppableData={droppableData}
|
||||||
draggableData={draggableData}
|
draggableData={draggableData}
|
||||||
|
isDragDisabled={isDragDisabled}
|
||||||
|
isDropDisabled={isDropDisabled}
|
||||||
isUploadDisabled={true}
|
isUploadDisabled={true}
|
||||||
fitContainer
|
fitContainer
|
||||||
useThumbailFallback
|
useThumbailFallback
|
||||||
@ -87,26 +103,13 @@ const CurrentImagePreview = () => {
|
|||||||
dataTestId="image-preview"
|
dataTestId="image-preview"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{shouldShowImageDetails && imageDTO && withMetadata && (
|
||||||
|
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
|
||||||
|
<ImageMetadataViewer image={imageDTO} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{shouldShowImageDetails && imageDTO && (
|
{withNextPrevButtons && shouldShowNextPrevButtons && imageDTO && (
|
||||||
<Box
|
|
||||||
as={motion.div}
|
|
||||||
key="metadataViewer"
|
|
||||||
initial={initial}
|
|
||||||
animate={animateMetadata}
|
|
||||||
exit={exit}
|
|
||||||
position="absolute"
|
|
||||||
top={0}
|
|
||||||
width="full"
|
|
||||||
height="full"
|
|
||||||
borderRadius="base"
|
|
||||||
>
|
|
||||||
<ImageMetadataViewer image={imageDTO} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
<AnimatePresence>
|
|
||||||
{shouldShowNextPrevButtons && imageDTO && (
|
|
||||||
<Box
|
<Box
|
||||||
as={motion.div}
|
as={motion.div}
|
||||||
key="nextPrevButtons"
|
key="nextPrevButtons"
|
||||||
@ -136,10 +139,6 @@ const animateArrows: AnimationProps['animate'] = {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: { duration: 0.07 },
|
transition: { duration: 0.07 },
|
||||||
};
|
};
|
||||||
const animateMetadata: AnimationProps['animate'] = {
|
|
||||||
opacity: 0.8,
|
|
||||||
transition: { duration: 0.07 },
|
|
||||||
};
|
|
||||||
const exit: AnimationProps['exit'] = {
|
const exit: AnimationProps['exit'] = {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transition: { duration: 0.07 },
|
transition: { duration: 0.07 },
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import { Button } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiArrowsDownUpBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
import { useImageViewer } from './useImageViewer';
|
|
||||||
|
|
||||||
const TAB_NAME_TO_TKEY_SHORT: Record<InvokeTabName, string> = {
|
|
||||||
generation: 'controlLayers.controlLayers',
|
|
||||||
canvas: 'ui.tabs.canvas',
|
|
||||||
workflows: 'ui.tabs.workflows',
|
|
||||||
models: 'ui.tabs.models',
|
|
||||||
queue: 'ui.tabs.queue',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditorButton = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { onClose } = useImageViewer();
|
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
|
||||||
const tooltip = useMemo(
|
|
||||||
() => t('gallery.switchTo', { tab: t(TAB_NAME_TO_TKEY_SHORT[activeTabName]) }),
|
|
||||||
[t, activeTabName]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
aria-label={tooltip}
|
|
||||||
tooltip={tooltip}
|
|
||||||
onClick={onClose}
|
|
||||||
variant="outline"
|
|
||||||
leftIcon={<PiArrowsDownUpBold />}
|
|
||||||
>
|
|
||||||
{t(TAB_NAME_TO_TKEY_SHORT[activeTabName])}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
@ -5,26 +5,12 @@ import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/To
|
|||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import type { AnimationProps } from 'framer-motion';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
import CurrentImageButtons from './CurrentImageButtons';
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
import CurrentImagePreview from './CurrentImagePreview';
|
import CurrentImagePreview from './CurrentImagePreview';
|
||||||
import { EditorButton } from './EditorButton';
|
import { ViewerToggleMenu } from './ViewerToggleMenu';
|
||||||
|
|
||||||
const initial: AnimationProps['initial'] = {
|
|
||||||
opacity: 0,
|
|
||||||
};
|
|
||||||
const animate: AnimationProps['animate'] = {
|
|
||||||
opacity: 1,
|
|
||||||
transition: { duration: 0.07 },
|
|
||||||
};
|
|
||||||
const exit: AnimationProps['exit'] = {
|
|
||||||
opacity: 0,
|
|
||||||
transition: { duration: 0.07 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
||||||
|
|
||||||
@ -42,50 +28,44 @@ export const ImageViewer = memo(() => {
|
|||||||
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
|
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
|
||||||
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
|
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
|
||||||
|
|
||||||
// The AnimatePresence mode must be wait - else framer can get confused if you spam the toggle button
|
if (!shouldShowViewer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence mode="wait">
|
<Flex
|
||||||
{shouldShowViewer && (
|
layerStyle="first"
|
||||||
<Flex
|
borderRadius="base"
|
||||||
key="imageViewer"
|
position="absolute"
|
||||||
as={motion.div}
|
flexDirection="column"
|
||||||
initial={initial}
|
top={0}
|
||||||
animate={animate}
|
right={0}
|
||||||
exit={exit}
|
bottom={0}
|
||||||
layerStyle="first"
|
left={0}
|
||||||
borderRadius="base"
|
p={2}
|
||||||
position="absolute"
|
rowGap={4}
|
||||||
flexDirection="column"
|
alignItems="center"
|
||||||
top={0}
|
justifyContent="center"
|
||||||
right={0}
|
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
|
||||||
bottom={0}
|
>
|
||||||
left={0}
|
<Flex w="full" gap={2}>
|
||||||
p={2}
|
<Flex flex={1} justifyContent="center">
|
||||||
rowGap={4}
|
<Flex gap={2} marginInlineEnd="auto">
|
||||||
alignItems="center"
|
<ToggleProgressButton />
|
||||||
justifyContent="center"
|
<ToggleMetadataViewerButton />
|
||||||
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
|
|
||||||
>
|
|
||||||
<Flex w="full" gap={2}>
|
|
||||||
<Flex flex={1} justifyContent="center">
|
|
||||||
<Flex gap={2} marginInlineEnd="auto">
|
|
||||||
<ToggleProgressButton />
|
|
||||||
<ToggleMetadataViewerButton />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
<Flex flex={1} gap={2} justifyContent="center">
|
|
||||||
<CurrentImageButtons />
|
|
||||||
</Flex>
|
|
||||||
<Flex flex={1} justifyContent="center">
|
|
||||||
<Flex gap={2} marginInlineStart="auto">
|
|
||||||
<EditorButton />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<CurrentImagePreview />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
<Flex flex={1} gap={2} justifyContent="center">
|
||||||
</AnimatePresence>
|
<CurrentImageButtons />
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} justifyContent="center">
|
||||||
|
<Flex gap={2} marginInlineStart="auto">
|
||||||
|
<ViewerToggleMenu />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<CurrentImagePreview />
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||||
|
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
|
import CurrentImagePreview from './CurrentImagePreview';
|
||||||
|
|
||||||
|
export const ImageViewerWorkflows = memo(() => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
layerStyle="first"
|
||||||
|
borderRadius="base"
|
||||||
|
position="absolute"
|
||||||
|
flexDirection="column"
|
||||||
|
top={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
left={0}
|
||||||
|
p={2}
|
||||||
|
rowGap={4}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
|
||||||
|
>
|
||||||
|
<Flex w="full" gap={2}>
|
||||||
|
<Flex flex={1} justifyContent="center">
|
||||||
|
<Flex gap={2} marginInlineEnd="auto">
|
||||||
|
<ToggleProgressButton />
|
||||||
|
<ToggleMetadataViewerButton />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} gap={2} justifyContent="center">
|
||||||
|
<CurrentImageButtons />
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} justifyContent="center">
|
||||||
|
<Flex gap={2} marginInlineStart="auto" />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<CurrentImagePreview />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageViewerWorkflows.displayName = 'ImageViewerWorkflows';
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user