mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-22 16:28:01 -05:00
Compare commits
118 Commits
v4.2.0
...
image-capt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59327e827b | ||
|
|
a18d7adad4 | ||
|
|
32dff2c4e3 | ||
|
|
575ecb4028 | ||
|
|
ad8778df6c | ||
|
|
d2f5103f9f | ||
|
|
dd42a56084 | ||
|
|
23ac340a3f | ||
|
|
6791b4eaa8 | ||
|
|
a8b042177d | ||
|
|
76825f4261 | ||
|
|
78cb4d75ad | ||
|
|
a18bbac262 | ||
|
|
9ff5596963 | ||
|
|
8ea596b1e9 | ||
|
|
e3a143eaed | ||
|
|
c359ab6d9b | ||
|
|
dbfaa07e03 | ||
|
|
7f78fe7a36 | ||
|
|
6cf5b402c6 | ||
|
|
b0c7c7cb47 | ||
|
|
4d68cd8dbb | ||
|
|
2c1fa30639 | ||
|
|
708c68413d | ||
|
|
1d884fb794 | ||
|
|
f6a44681a8 | ||
|
|
d4df312300 | ||
|
|
9c0d44b412 | ||
|
|
27826369f0 | ||
|
|
31d8b50276 | ||
|
|
40b4fa7238 | ||
|
|
3b1743b7c2 | ||
|
|
f489c818f1 | ||
|
|
af477fa295 | ||
|
|
0ff0290735 | ||
|
|
67dbe6d949 | ||
|
|
4c3c2297b9 | ||
|
|
cadea55521 | ||
|
|
c8f30b1392 | ||
|
|
3d14a98abf | ||
|
|
77024bfca7 | ||
|
|
4a1c3786a1 | ||
|
|
b239891986 | ||
|
|
9fb03d43ff | ||
|
|
bdc59786bd | ||
|
|
fb6e926500 | ||
|
|
48ccd63dba | ||
|
|
ee647a05dc | ||
|
|
154b52ca4d | ||
|
|
5dd460c3ce | ||
|
|
4897ce2a13 | ||
|
|
5425526d50 | ||
|
|
5a4b050e66 | ||
|
|
8d39520232 | ||
|
|
04d12a1e98 | ||
|
|
39aa70963b | ||
|
|
5743254a41 | ||
|
|
c538ffea26 | ||
|
|
e8d3a7c870 | ||
|
|
2be66b1546 | ||
|
|
76e181fd44 | ||
|
|
b5d42fbc66 | ||
|
|
b463cd763e | ||
|
|
eb320df41d | ||
|
|
de1869773f | ||
|
|
ef89c7e537 | ||
|
|
008645d386 | ||
|
|
f8042ffb41 | ||
|
|
dbe22be598 | ||
|
|
8f6078d007 | ||
|
|
4020bf47e2 | ||
|
|
9d685da759 | ||
|
|
e3289856c0 | ||
|
|
47b8153728 | ||
|
|
7901e4c082 | ||
|
|
18b0977a31 | ||
|
|
fc6b214470 | ||
|
|
e22211dac0 | ||
|
|
e222484663 | ||
|
|
2a9cea6689 | ||
|
|
93da75209c | ||
|
|
9c819f0fd8 | ||
|
|
eef6fcf286 | ||
|
|
e375d9f787 | ||
|
|
ab18174774 | ||
|
|
9265841384 | ||
|
|
c5fd08125d | ||
|
|
11d88dae7f | ||
|
|
3b495659b0 | ||
|
|
15c9a3a4b6 | ||
|
|
60e77e4ed6 | ||
|
|
fa832a8ac6 | ||
|
|
f7834d7d59 | ||
|
|
63d7461510 | ||
|
|
1de704160e | ||
|
|
b118a2565c | ||
|
|
eb166baafe | ||
|
|
818d37f304 | ||
|
|
9cdb801c1c | ||
|
|
5da8cde4fc | ||
|
|
6ec3dc0c0d | ||
|
|
6050dffb25 | ||
|
|
93efeafe30 | ||
|
|
f167e8a8d3 | ||
|
|
124d49f35e | ||
|
|
52d8efa892 | ||
|
|
4ea8416c68 | ||
|
|
8dd0bfb068 | ||
|
|
6ff1c7d541 | ||
|
|
19f5a9c3a9 | ||
|
|
d9ce9c62ac | ||
|
|
cdc468a38c | ||
|
|
2656f13a4a | ||
|
|
da61396b1c | ||
|
|
6c9fb617dc | ||
|
|
5dd73fe53e | ||
|
|
e6793be465 | ||
|
|
63e62c5720 |
@@ -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,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,11 +1,11 @@
|
|||||||
# 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, List, Union
|
||||||
from typing import Literal, Optional
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy
|
import numpy
|
||||||
from PIL import Image, ImageChops, ImageFilter, ImageOps
|
from PIL import Image, ImageChops, ImageFilter, ImageOps
|
||||||
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||||
|
|
||||||
from invokeai.app.invocations.constants import IMAGE_MODES
|
from invokeai.app.invocations.constants import IMAGE_MODES
|
||||||
from invokeai.app.invocations.fields import (
|
from invokeai.app.invocations.fields import (
|
||||||
@@ -16,7 +16,7 @@ from invokeai.app.invocations.fields import (
|
|||||||
WithBoard,
|
WithBoard,
|
||||||
WithMetadata,
|
WithMetadata,
|
||||||
)
|
)
|
||||||
from invokeai.app.invocations.primitives import ImageOutput
|
from invokeai.app.invocations.primitives import ImageOutput, CaptionImageOutputs, CaptionImageOutput
|
||||||
from invokeai.app.services.image_records.image_records_common import ImageCategory
|
from invokeai.app.services.image_records.image_records_common import ImageCategory
|
||||||
from invokeai.app.services.shared.invocation_context import InvocationContext
|
from invokeai.app.services.shared.invocation_context import InvocationContext
|
||||||
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
|
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
|
||||||
@@ -67,6 +67,56 @@ class BlankImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
|||||||
return ImageOutput.build(image_dto)
|
return ImageOutput.build(image_dto)
|
||||||
|
|
||||||
|
|
||||||
|
@invocation(
|
||||||
|
"auto_caption_image",
|
||||||
|
title="Automatically Caption Image",
|
||||||
|
tags=["image", "caption"],
|
||||||
|
category="image",
|
||||||
|
version="1.2.2",
|
||||||
|
)
|
||||||
|
class CaptionImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||||
|
"""Adds a caption to an image"""
|
||||||
|
|
||||||
|
images: Union[ImageField,List[ImageField]] = InputField(description="The image to caption")
|
||||||
|
prompt: str = InputField(default="Describe this list of images in 20 words or less", description="Describe how you would like the image to be captioned.")
|
||||||
|
|
||||||
|
def invoke(self, context: InvocationContext) -> CaptionImageOutputs:
|
||||||
|
|
||||||
|
model_id = "vikhyatk/moondream2"
|
||||||
|
model_revision = "2024-04-02"
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(model_id, revision=model_revision)
|
||||||
|
moondream_model = AutoModelForCausalLM.from_pretrained(
|
||||||
|
model_id, trust_remote_code=True, revision=model_revision
|
||||||
|
)
|
||||||
|
output: CaptionImageOutputs = CaptionImageOutputs()
|
||||||
|
try:
|
||||||
|
from PIL.Image import Image
|
||||||
|
images: List[Image] = []
|
||||||
|
image_fields = self.images if isinstance(self.images, list) else [self.images]
|
||||||
|
for image in image_fields:
|
||||||
|
images.append(context.images.get_pil(image.image_name))
|
||||||
|
answers: List[str] = moondream_model.batch_answer(
|
||||||
|
images=images,
|
||||||
|
prompts=[self.prompt] * len(images),
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
)
|
||||||
|
assert isinstance(answers, list)
|
||||||
|
for i, answer in enumerate(answers):
|
||||||
|
output.images.append(CaptionImageOutput(
|
||||||
|
image=image_fields[i],
|
||||||
|
width=images[i].width,
|
||||||
|
height=images[i].height,
|
||||||
|
caption=answer
|
||||||
|
))
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
del moondream_model
|
||||||
|
del tokenizer
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
@invocation(
|
@invocation(
|
||||||
"img_crop",
|
"img_crop",
|
||||||
title="Crop Image",
|
title="Crop Image",
|
||||||
@@ -195,7 +245,7 @@ class ImagePasteInvocation(BaseInvocation, WithMetadata, WithBoard):
|
|||||||
class MaskFromAlphaInvocation(BaseInvocation, WithMetadata, WithBoard):
|
class MaskFromAlphaInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||||
"""Extracts the alpha channel of an image as a mask."""
|
"""Extracts the alpha channel of an image as a mask."""
|
||||||
|
|
||||||
image: ImageField = InputField(description="The image to create the mask from")
|
image: List[ImageField] = InputField(description="The image to create the mask from")
|
||||||
invert: bool = InputField(default=False, description="Whether or not to invert the mask")
|
invert: bool = InputField(default=False, description="Whether or not to invert the mask")
|
||||||
|
|
||||||
def invoke(self, context: InvocationContext) -> ImageOutput:
|
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||||
@@ -504,7 +554,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 +566,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"""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654)
|
# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654)
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
@@ -247,6 +247,17 @@ class ImageOutput(BaseInvocationOutput):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@invocation_output("captioned_image_output")
|
||||||
|
class CaptionImageOutput(ImageOutput):
|
||||||
|
caption: str = OutputField(description="Caption for given image")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@invocation_output("captioned_image_outputs")
|
||||||
|
class CaptionImageOutputs(BaseInvocationOutput):
|
||||||
|
images: List[CaptionImageOutput] = OutputField(description="List of captioned images", default=[])
|
||||||
|
|
||||||
|
|
||||||
@invocation_output("image_collection_output")
|
@invocation_output("image_collection_output")
|
||||||
class ImageCollectionOutput(BaseInvocationOutput):
|
class ImageCollectionOutput(BaseInvocationOutput):
|
||||||
"""Base class for nodes that output a collection of images"""
|
"""Base class for nodes that output a collection of images"""
|
||||||
|
|||||||
@@ -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: [
|
||||||
/**
|
/**
|
||||||
|
|||||||
1
invokeai/frontend/web/.gitignore
vendored
1
invokeai/frontend/web/.gitignore
vendored
@@ -44,3 +44,4 @@ 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": {
|
||||||
@@ -132,6 +133,8 @@
|
|||||||
"@types/react-dom": "^18.3.0",
|
"@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",
|
||||||
|
|||||||
148
invokeai/frontend/web/pnpm-lock.yaml
generated
148
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -229,6 +229,12 @@ devDependencies:
|
|||||||
'@vitejs/plugin-react-swc':
|
'@vitejs/plugin-react-swc':
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 3.6.0(vite@5.2.11)
|
version: 3.6.0(vite@5.2.11)
|
||||||
|
'@vitest/coverage-v8':
|
||||||
|
specifier: ^1.5.0
|
||||||
|
version: 1.6.0(vitest@1.6.0)
|
||||||
|
'@vitest/ui':
|
||||||
|
specifier: ^1.5.0
|
||||||
|
version: 1.6.0(vitest@1.6.0)
|
||||||
concurrently:
|
concurrently:
|
||||||
specifier: ^8.2.2
|
specifier: ^8.2.2
|
||||||
version: 8.2.2
|
version: 8.2.2
|
||||||
@@ -288,7 +294,7 @@ devDependencies:
|
|||||||
version: 4.3.2(typescript@5.4.5)(vite@5.2.11)
|
version: 4.3.2(typescript@5.4.5)(vite@5.2.11)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^1.6.0
|
specifier: ^1.6.0
|
||||||
version: 1.6.0(@types/node@20.12.10)
|
version: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1679,6 +1685,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==}
|
resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@bcoe/v8-coverage@0.2.3:
|
||||||
|
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1):
|
/@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==}
|
resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3635,6 +3645,11 @@ packages:
|
|||||||
wrap-ansi-cjs: /wrap-ansi@7.0.0
|
wrap-ansi-cjs: /wrap-ansi@7.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@istanbuljs/schema@0.1.3:
|
||||||
|
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@jest/schemas@29.6.3:
|
/@jest/schemas@29.6.3:
|
||||||
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
@@ -3822,6 +3837,10 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@polka/url@1.0.0-next.25:
|
||||||
|
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@popperjs/core@2.11.8:
|
/@popperjs/core@2.11.8:
|
||||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -5146,7 +5165,7 @@ packages:
|
|||||||
dom-accessibility-api: 0.6.3
|
dom-accessibility-api: 0.6.3
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
redent: 3.0.0
|
redent: 3.0.0
|
||||||
vitest: 1.6.0(@types/node@20.12.10)
|
vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4):
|
/@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4):
|
||||||
@@ -5825,6 +5844,29 @@ packages:
|
|||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@vitest/coverage-v8@1.6.0(vitest@1.6.0):
|
||||||
|
resolution: {integrity: sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==}
|
||||||
|
peerDependencies:
|
||||||
|
vitest: 1.6.0
|
||||||
|
dependencies:
|
||||||
|
'@ampproject/remapping': 2.3.0
|
||||||
|
'@bcoe/v8-coverage': 0.2.3
|
||||||
|
debug: 4.3.4
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
istanbul-lib-report: 3.0.1
|
||||||
|
istanbul-lib-source-maps: 5.0.4
|
||||||
|
istanbul-reports: 3.1.7
|
||||||
|
magic-string: 0.30.10
|
||||||
|
magicast: 0.3.4
|
||||||
|
picocolors: 1.0.0
|
||||||
|
std-env: 3.7.0
|
||||||
|
strip-literal: 2.1.0
|
||||||
|
test-exclude: 6.0.0
|
||||||
|
vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@vitest/expect@1.3.1:
|
/@vitest/expect@1.3.1:
|
||||||
resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==}
|
resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5869,6 +5911,21 @@ packages:
|
|||||||
tinyspy: 2.2.1
|
tinyspy: 2.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@vitest/ui@1.6.0(vitest@1.6.0):
|
||||||
|
resolution: {integrity: sha512-k3Lyo+ONLOgylctiGovRKy7V4+dIN2yxstX3eY5cWFXH6WP+ooVX79YSyi0GagdTQzLmT43BF27T0s6dOIPBXA==}
|
||||||
|
peerDependencies:
|
||||||
|
vitest: 1.6.0
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 1.6.0
|
||||||
|
fast-glob: 3.3.2
|
||||||
|
fflate: 0.8.2
|
||||||
|
flatted: 3.3.1
|
||||||
|
pathe: 1.1.2
|
||||||
|
picocolors: 1.0.0
|
||||||
|
sirv: 2.0.4
|
||||||
|
vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0)
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@vitest/utils@1.3.1:
|
/@vitest/utils@1.3.1:
|
||||||
resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==}
|
resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8521,6 +8578,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==}
|
resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/fflate@0.8.2:
|
||||||
|
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/file-entry-cache@6.0.1:
|
/file-entry-cache@6.0.1:
|
||||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
engines: {node: ^10.12.0 || >=12.0.0}
|
||||||
@@ -9084,6 +9145,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/html-escaper@2.0.2:
|
||||||
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/html-parse-stringify@3.0.1:
|
/html-parse-stringify@3.0.1:
|
||||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9513,6 +9578,39 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/istanbul-lib-coverage@3.2.2:
|
||||||
|
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/istanbul-lib-report@3.0.1:
|
||||||
|
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
make-dir: 4.0.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/istanbul-lib-source-maps@5.0.4:
|
||||||
|
resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/trace-mapping': 0.3.25
|
||||||
|
debug: 4.3.4
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/istanbul-reports@3.1.7:
|
||||||
|
resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
html-escaper: 2.0.2
|
||||||
|
istanbul-lib-report: 3.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/iterable-lookahead@1.0.0:
|
/iterable-lookahead@1.0.0:
|
||||||
resolution: {integrity: sha512-hJnEP2Xk4+44DDwJqUQGdXal5VbyeWLaPyDl2AQc242Zr7iqz4DgpQOrEzglWVMGHMDCkguLHEKxd1+rOsmgSQ==}
|
resolution: {integrity: sha512-hJnEP2Xk4+44DDwJqUQGdXal5VbyeWLaPyDl2AQc242Zr7iqz4DgpQOrEzglWVMGHMDCkguLHEKxd1+rOsmgSQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -9912,6 +10010,14 @@ packages:
|
|||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/magicast@0.3.4:
|
||||||
|
resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.24.5
|
||||||
|
'@babel/types': 7.24.5
|
||||||
|
source-map-js: 1.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/make-dir@2.1.0:
|
/make-dir@2.1.0:
|
||||||
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
|
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -9927,6 +10033,13 @@ packages:
|
|||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/make-dir@4.0.0:
|
||||||
|
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
semver: 7.6.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/map-obj@2.0.0:
|
/map-obj@2.0.0:
|
||||||
resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==}
|
resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -10101,6 +10214,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
|
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/mrmime@2.0.0:
|
||||||
|
resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ms@2.0.0:
|
/ms@2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -11766,6 +11884,15 @@ packages:
|
|||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/sirv@2.0.4:
|
||||||
|
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
dependencies:
|
||||||
|
'@polka/url': 1.0.0-next.25
|
||||||
|
mrmime: 2.0.0
|
||||||
|
totalist: 3.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/sisteransi@1.0.5:
|
/sisteransi@1.0.5:
|
||||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -12191,6 +12318,15 @@ packages:
|
|||||||
unique-string: 2.0.0
|
unique-string: 2.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/test-exclude@6.0.0:
|
||||||
|
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
'@istanbuljs/schema': 0.1.3
|
||||||
|
glob: 7.2.3
|
||||||
|
minimatch: 3.1.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/text-table@0.2.0:
|
/text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -12264,6 +12400,11 @@ packages:
|
|||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/totalist@3.0.1:
|
||||||
|
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tr46@0.0.3:
|
/tr46@0.0.3:
|
||||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
|
||||||
@@ -12837,7 +12978,7 @@ packages:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vitest@1.6.0(@types/node@20.12.10):
|
/vitest@1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0):
|
||||||
resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
|
resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -12867,6 +13008,7 @@ packages:
|
|||||||
'@vitest/runner': 1.6.0
|
'@vitest/runner': 1.6.0
|
||||||
'@vitest/snapshot': 1.6.0
|
'@vitest/snapshot': 1.6.0
|
||||||
'@vitest/spy': 1.6.0
|
'@vitest/spy': 1.6.0
|
||||||
|
'@vitest/ui': 1.6.0(vitest@1.6.0)
|
||||||
'@vitest/utils': 1.6.0
|
'@vitest/utils': 1.6.0
|
||||||
acorn-walk: 8.3.2
|
acorn-walk: 8.3.2
|
||||||
chai: 4.4.1
|
chai: 4.4.1
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -261,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",
|
||||||
@@ -314,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",
|
||||||
@@ -769,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",
|
||||||
@@ -934,7 +940,20 @@
|
|||||||
"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",
|
||||||
@@ -945,8 +964,6 @@
|
|||||||
"positivePromptPlaceholder": "Positive Prompt",
|
"positivePromptPlaceholder": "Positive Prompt",
|
||||||
"globalPositivePromptPlaceholder": "Global 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",
|
||||||
|
|||||||
@@ -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,13 +1,14 @@
|
|||||||
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,
|
caLayerRecalled,
|
||||||
isControlAdapterLayer,
|
isControlAdapterLayer,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
@@ -15,47 +16,39 @@ 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, caLayerRecalled);
|
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 = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId;
|
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();
|
||||||
@@ -63,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 = {
|
||||||
@@ -83,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: [],
|
||||||
},
|
},
|
||||||
@@ -91,16 +100,21 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
// Kick off the processor batch
|
||||||
const req = dispatch(
|
const req = dispatch(
|
||||||
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
|
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
|
||||||
fixedCacheKey: 'enqueueBatch',
|
fixedCacheKey: 'enqueueBatch',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
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) &&
|
||||||
@@ -109,31 +123,32 @@ 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(
|
||||||
|
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;
|
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 }));
|
||||||
// Update the processed image in the store
|
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
|
||||||
dispatch(
|
|
||||||
caLayerProcessedImageChanged({
|
|
||||||
layerId,
|
|
||||||
imageDTO,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false }));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
// The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now).
|
||||||
|
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'));
|
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
|
||||||
dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false }));
|
|
||||||
|
|
||||||
if (error instanceof Object) {
|
if (error instanceof Object) {
|
||||||
if ('data' in error && 'status' in error) {
|
if ('data' in error && 'status' in error) {
|
||||||
@@ -151,6 +166,9 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} 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,9 +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 { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph';
|
|
||||||
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) => {
|
||||||
@@ -18,7 +18,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
|
|
||||||
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);
|
||||||
@@ -32,7 +32,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
req.unwrap();
|
await req.unwrap();
|
||||||
if (shouldShowProgressInViewer) {
|
if (shouldShowProgressInViewer) {
|
||||||
dispatch(isImageViewerOpenChanged(true));
|
dispatch(isImageViewerOpenChanged(true));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
|
await req.unwrap();
|
||||||
|
} finally {
|
||||||
req.reset();
|
req.reset();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,5 +1,6 @@
|
|||||||
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 {
|
import {
|
||||||
@@ -9,7 +10,9 @@ import {
|
|||||||
isImageViewerOpenChanged,
|
isImageViewerOpenChanged,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} 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';
|
||||||
@@ -28,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;
|
||||||
@@ -110,6 +113,16 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,6 +2,7 @@ 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 { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
|
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
|
||||||
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
|
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
|
||||||
@@ -14,10 +15,10 @@ import { fromZodError } from 'zod-validation-error';
|
|||||||
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 { workflow, asCopy } = action.payload;
|
||||||
const nodeTemplates = getState().nodes.templates;
|
const nodeTemplates = $templates.get();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { workflow: validatedWorkflow, warnings } = validateWorkflow(workflow, nodeTemplates);
|
const { workflow: validatedWorkflow, warnings } = validateWorkflow(workflow, nodeTemplates);
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,44 +7,58 @@ 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',
|
||||||
|
control_adapter_layer: 'controlLayers.globalControlAdapter',
|
||||||
|
ip_adapter_layer: 'controlLayers.globalIPAdapter',
|
||||||
|
regional_guidance_layer: 'controlLayers.regionalGuidance',
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSelector = (templates: Templates) =>
|
||||||
|
createMemoizedSelector(
|
||||||
[
|
[
|
||||||
selectControlAdaptersSlice,
|
selectControlAdaptersSlice,
|
||||||
selectGenerationSlice,
|
selectGenerationSlice,
|
||||||
selectSystemSlice,
|
selectSystemSlice,
|
||||||
selectNodesSlice,
|
selectNodesSlice,
|
||||||
|
selectWorkflowSettingsSlice,
|
||||||
selectDynamicPromptsSlice,
|
selectDynamicPromptsSlice,
|
||||||
selectControlLayersSlice,
|
selectControlLayersSlice,
|
||||||
activeTabNameSelector,
|
activeTabNameSelector,
|
||||||
],
|
],
|
||||||
(controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => {
|
(controlAdapters, generation, system, nodes, workflowSettings, dynamicPrompts, controlLayers, activeTabName) => {
|
||||||
const { model } = generation;
|
const { model } = generation;
|
||||||
|
const { size } = controlLayers.present;
|
||||||
const { positivePrompt } = controlLayers.present;
|
const { positivePrompt } = controlLayers.present;
|
||||||
|
|
||||||
const { isConnected } = system;
|
const { isConnected } = system;
|
||||||
|
|
||||||
const reasons: string[] = [];
|
const reasons: { prefix?: string; content: string }[] = [];
|
||||||
|
|
||||||
// Cannot generate if not connected
|
// Cannot generate if not connected
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
reasons.push(i18n.t('parameters.invoke.systemDisconnected'));
|
reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTabName === 'workflows') {
|
if (activeTabName === 'workflows') {
|
||||||
if (nodes.shouldValidateGraph) {
|
if (workflowSettings.shouldValidateGraph) {
|
||||||
if (!nodes.nodes.length) {
|
if (!nodes.nodes.length) {
|
||||||
reasons.push(i18n.t('parameters.invoke.noNodesInGraph'));
|
reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') });
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.nodes.forEach((node) => {
|
nodes.nodes.forEach((node) => {
|
||||||
@@ -51,11 +66,11 @@ const selector = createMemoizedSelector(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeTemplate = nodes.templates[node.data.type];
|
const nodeTemplate = templates[node.data.type];
|
||||||
|
|
||||||
if (!nodeTemplate) {
|
if (!nodeTemplate) {
|
||||||
// Node type not found
|
// Node type not found
|
||||||
reasons.push(i18n.t('parameters.invoke.missingNodeTemplate'));
|
reasons.push({ content: i18n.t('parameters.invoke.missingNodeTemplate') });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,17 +83,17 @@ const selector = createMemoizedSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!fieldTemplate) {
|
if (!fieldTemplate) {
|
||||||
reasons.push(i18n.t('parameters.invoke.missingFieldTemplate'));
|
reasons.push({ content: i18n.t('parameters.invoke.missingFieldTemplate') });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldTemplate.required && field.value === undefined && !hasConnection) {
|
if (fieldTemplate.required && field.value === undefined && !hasConnection) {
|
||||||
reasons.push(
|
reasons.push({
|
||||||
i18n.t('parameters.invoke.missingInputForField', {
|
content: i18n.t('parameters.invoke.missingInputForField', {
|
||||||
nodeLabel: node.data.label || nodeTemplate.title,
|
nodeLabel: node.data.label || nodeTemplate.title,
|
||||||
fieldLabel: field.label || fieldTemplate.title,
|
fieldLabel: field.label || fieldTemplate.title,
|
||||||
})
|
}),
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -86,62 +101,97 @@ const selector = createMemoizedSelector(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
|
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
|
||||||
reasons.push(i18n.t('parameters.invoke.noPrompts'));
|
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
reasons.push(i18n.t('parameters.invoke.noModelSelected'));
|
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTabName === 'generation') {
|
if (activeTabName === 'generation') {
|
||||||
// Handling for generation tab
|
// Handling for generation tab
|
||||||
controlLayers.present.layers
|
controlLayers.present.layers
|
||||||
.filter((l) => l.isEnabled)
|
.filter((l) => l.isEnabled)
|
||||||
.flatMap((l) => {
|
.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') {
|
if (l.type === 'control_adapter_layer') {
|
||||||
return l.controlAdapter;
|
// Must have model
|
||||||
} else if (l.type === 'ip_adapter_layer') {
|
if (!l.controlAdapter.model) {
|
||||||
return l.ipAdapter;
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
|
||||||
} else if (l.type === 'regional_guidance_layer') {
|
}
|
||||||
return l.ipAdapters;
|
// 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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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) {
|
if (l.type === 'ip_adapter_layer') {
|
||||||
reasons.push(
|
// Must have model
|
||||||
i18n.t('parameters.invoke.noModelForControlAdapter', {
|
if (!l.ipAdapter.model) {
|
||||||
number: i + 1,
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (mismatchedModelBase) {
|
// Model base must match
|
||||||
// This should never happen, just a sanity check
|
if (l.ipAdapter.model?.base !== model?.base) {
|
||||||
reasons.push(
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
|
||||||
i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (hasNoImage) {
|
// Must have an image
|
||||||
reasons.push(
|
if (!l.ipAdapter.image) {
|
||||||
i18n.t('parameters.invoke.noControlImageForControlAdapter', {
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (imageNotProcessed) {
|
}
|
||||||
reasons.push(
|
|
||||||
i18n.t('parameters.invoke.imageNotProcessedForControlAdapter', {
|
if (l.type === 'initial_image_layer') {
|
||||||
number: i + 1,
|
// Must have an image
|
||||||
})
|
if (!l.image) {
|
||||||
);
|
problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l.type === 'regional_guidance_layer') {
|
||||||
|
// Must have a region
|
||||||
|
if (l.maskObjects.length === 0) {
|
||||||
|
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 (problems.length) {
|
||||||
|
const content = upperFirst(problems.join(', '));
|
||||||
|
reasons.push({ prefix, content });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -154,29 +204,21 @@ const selector = createMemoizedSelector(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ca.model) {
|
if (!ca.model) {
|
||||||
reasons.push(
|
reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) });
|
||||||
i18n.t('parameters.invoke.noModelForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (ca.model.base !== model?.base) {
|
} else if (ca.model.base !== model?.base) {
|
||||||
// This should never happen, just a sanity check
|
// This should never happen, just a sanity check
|
||||||
reasons.push(
|
reasons.push({
|
||||||
i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', {
|
content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }),
|
||||||
number: i + 1,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!ca.controlImage ||
|
!ca.controlImage ||
|
||||||
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
|
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
|
||||||
) {
|
) {
|
||||||
reasons.push(
|
reasons.push({
|
||||||
i18n.t('parameters.invoke.noControlImageForControlAdapter', {
|
content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }),
|
||||||
number: i + 1,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -187,6 +229,8 @@ const selector = createMemoizedSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
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,7 +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 { 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';
|
||||||
@@ -216,13 +215,20 @@ 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 alignItems="center" gap={2} flexWrap="wrap">
|
<Flex alignItems="center" gap={2} flexWrap="wrap">
|
||||||
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
|
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
|
||||||
<FormControl isDisabled={isStaging} w="5rem">
|
<FormControl isDisabled={isStaging} w="5rem">
|
||||||
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} />
|
<Combobox value={layerValue} options={layerOptions} onChange={handleChangeLayer} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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: 2,
|
_version: 3,
|
||||||
selectedLayerId: null,
|
selectedLayerId: null,
|
||||||
brushSize: 100,
|
brushSize: 100,
|
||||||
layers: [],
|
layers: [],
|
||||||
@@ -334,13 +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;
|
||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
@@ -800,7 +800,7 @@ export const {
|
|||||||
caLayerProcessorConfigChanged,
|
caLayerProcessorConfigChanged,
|
||||||
caLayerIsFilterEnabledChanged,
|
caLayerIsFilterEnabledChanged,
|
||||||
caLayerOpacityChanged,
|
caLayerOpacityChanged,
|
||||||
caLayerIsProcessingImageChanged,
|
caLayerProcessorPendingBatchIdChanged,
|
||||||
// IPA Layers
|
// IPA Layers
|
||||||
ipaLayerAdded,
|
ipaLayerAdded,
|
||||||
ipaLayerRecalled,
|
ipaLayerRecalled,
|
||||||
@@ -857,7 +857,16 @@ export const selectControlLayersSlice = (state: RootState) => state.controlLayer
|
|||||||
const migrateControlLayersState = (state: any): any => {
|
const migrateControlLayersState = (state: any): any => {
|
||||||
if (state._version === 1) {
|
if (state._version === 1) {
|
||||||
// Reset state for users on v1 (e.g. beta users), some changes could cause
|
// Reset state for users on v1 (e.g. beta users), some changes could cause
|
||||||
return deepClone(initialControlLayersState);
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export const zLayer = z.discriminatedUnion('type', [
|
|||||||
export type Layer = z.infer<typeof zLayer>;
|
export type Layer = z.infer<typeof zLayer>;
|
||||||
|
|
||||||
export type ControlLayersState = {
|
export type ControlLayersState = {
|
||||||
_version: 2;
|
_version: 3;
|
||||||
selectedLayerId: string | null;
|
selectedLayerId: string | null;
|
||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
brushSize: number;
|
brushSize: number;
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
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,
|
|
||||||
_ColorMapProcessorConfig,
|
|
||||||
_ContentShuffleProcessorConfig,
|
|
||||||
_DepthAnythingProcessorConfig,
|
|
||||||
_DWOpenposeProcessorConfig,
|
|
||||||
_HedProcessorConfig,
|
|
||||||
_LineartAnimeProcessorConfig,
|
|
||||||
_LineartProcessorConfig,
|
|
||||||
_MediapipeFaceProcessorConfig,
|
|
||||||
_MidasDepthProcessorConfig,
|
|
||||||
_MlsdProcessorConfig,
|
|
||||||
_NormalbaeProcessorConfig,
|
|
||||||
_PidiProcessorConfig,
|
|
||||||
_ZoeDepthProcessorConfig,
|
|
||||||
CannyProcessorConfig,
|
CannyProcessorConfig,
|
||||||
CLIPVisionModelV2,
|
CLIPVisionModelV2,
|
||||||
ColorMapProcessorConfig,
|
ColorMapProcessorConfig,
|
||||||
@@ -45,16 +31,16 @@ describe('Control Adapter Types', () => {
|
|||||||
assert<Equals<ProcessorConfig['type'], ProcessorTypeV2>>();
|
assert<Equals<ProcessorConfig['type'], ProcessorTypeV2>>();
|
||||||
});
|
});
|
||||||
test('IP Adapter Method', () => {
|
test('IP Adapter Method', () => {
|
||||||
assert<Equals<NonNullable<S['IPAdapterInvocation']['method']>, IPMethodV2>>();
|
assert<Equals<NonNullable<Invocation<'ip_adapter'>['method']>, IPMethodV2>>();
|
||||||
});
|
});
|
||||||
test('CLIP Vision Model', () => {
|
test('CLIP Vision Model', () => {
|
||||||
assert<Equals<NonNullable<S['IPAdapterInvocation']['clip_vision_model']>, CLIPVisionModelV2>>();
|
assert<Equals<NonNullable<Invocation<'ip_adapter'>['clip_vision_model']>, CLIPVisionModelV2>>();
|
||||||
});
|
});
|
||||||
test('Control Mode', () => {
|
test('Control Mode', () => {
|
||||||
assert<Equals<NonNullable<S['ControlNetInvocation']['control_mode']>, ControlModeV2>>();
|
assert<Equals<NonNullable<Invocation<'controlnet'>['control_mode']>, ControlModeV2>>();
|
||||||
});
|
});
|
||||||
test('DepthAnything Model Size', () => {
|
test('DepthAnything Model Size', () => {
|
||||||
assert<Equals<NonNullable<S['DepthAnythingImageProcessorInvocation']['model_size']>, DepthAnythingModelSize>>();
|
assert<Equals<NonNullable<Invocation<'depth_anything_image_processor'>['model_size']>, DepthAnythingModelSize>>();
|
||||||
});
|
});
|
||||||
test('Processor Configs', () => {
|
test('Processor Configs', () => {
|
||||||
// The processor configs are manually modeled zod schemas. This test ensures that the inferred types are correct.
|
// The processor configs are manually modeled zod schemas. This test ensures that the inferred types are correct.
|
||||||
@@ -75,3 +61,33 @@ describe('Control Adapter Types', () => {
|
|||||||
assert<Equals<_ZoeDepthProcessorConfig, ZoeDepthProcessorConfig>>();
|
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,27 +1,7 @@
|
|||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||||
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 zId = z.string().min(1);
|
||||||
@@ -32,9 +12,6 @@ const zCannyProcessorConfig = z.object({
|
|||||||
low_threshold: z.number().int().gte(0).lte(255),
|
low_threshold: z.number().int().gte(0).lte(255),
|
||||||
high_threshold: z.number().int().gte(0).lte(255),
|
high_threshold: z.number().int().gte(0).lte(255),
|
||||||
});
|
});
|
||||||
export type _CannyProcessorConfig = Required<
|
|
||||||
Pick<CannyImageProcessorInvocation, 'id' | 'type' | 'low_threshold' | 'high_threshold'>
|
|
||||||
>;
|
|
||||||
export type CannyProcessorConfig = z.infer<typeof zCannyProcessorConfig>;
|
export type CannyProcessorConfig = z.infer<typeof zCannyProcessorConfig>;
|
||||||
|
|
||||||
const zColorMapProcessorConfig = z.object({
|
const zColorMapProcessorConfig = z.object({
|
||||||
@@ -42,9 +19,6 @@ const zColorMapProcessorConfig = z.object({
|
|||||||
type: z.literal('color_map_image_processor'),
|
type: z.literal('color_map_image_processor'),
|
||||||
color_map_tile_size: z.number().int().gte(1),
|
color_map_tile_size: z.number().int().gte(1),
|
||||||
});
|
});
|
||||||
export type _ColorMapProcessorConfig = Required<
|
|
||||||
Pick<ColorMapImageProcessorInvocation, 'id' | 'type' | 'color_map_tile_size'>
|
|
||||||
>;
|
|
||||||
export type ColorMapProcessorConfig = z.infer<typeof zColorMapProcessorConfig>;
|
export type ColorMapProcessorConfig = z.infer<typeof zColorMapProcessorConfig>;
|
||||||
|
|
||||||
const zContentShuffleProcessorConfig = z.object({
|
const zContentShuffleProcessorConfig = z.object({
|
||||||
@@ -54,9 +28,6 @@ const zContentShuffleProcessorConfig = z.object({
|
|||||||
h: z.number().int().gte(0),
|
h: z.number().int().gte(0),
|
||||||
f: z.number().int().gte(0),
|
f: z.number().int().gte(0),
|
||||||
});
|
});
|
||||||
export type _ContentShuffleProcessorConfig = Required<
|
|
||||||
Pick<ContentShuffleImageProcessorInvocation, 'id' | 'type' | 'w' | 'h' | 'f'>
|
|
||||||
>;
|
|
||||||
export type ContentShuffleProcessorConfig = z.infer<typeof zContentShuffleProcessorConfig>;
|
export type ContentShuffleProcessorConfig = z.infer<typeof zContentShuffleProcessorConfig>;
|
||||||
|
|
||||||
const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']);
|
const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']);
|
||||||
@@ -68,9 +39,6 @@ const zDepthAnythingProcessorConfig = z.object({
|
|||||||
type: z.literal('depth_anything_image_processor'),
|
type: z.literal('depth_anything_image_processor'),
|
||||||
model_size: zDepthAnythingModelSize,
|
model_size: zDepthAnythingModelSize,
|
||||||
});
|
});
|
||||||
export type _DepthAnythingProcessorConfig = Required<
|
|
||||||
Pick<DepthAnythingImageProcessorInvocation, 'id' | 'type' | 'model_size'>
|
|
||||||
>;
|
|
||||||
export type DepthAnythingProcessorConfig = z.infer<typeof zDepthAnythingProcessorConfig>;
|
export type DepthAnythingProcessorConfig = z.infer<typeof zDepthAnythingProcessorConfig>;
|
||||||
|
|
||||||
const zHedProcessorConfig = z.object({
|
const zHedProcessorConfig = z.object({
|
||||||
@@ -78,14 +46,12 @@ const zHedProcessorConfig = z.object({
|
|||||||
type: z.literal('hed_image_processor'),
|
type: z.literal('hed_image_processor'),
|
||||||
scribble: z.boolean(),
|
scribble: z.boolean(),
|
||||||
});
|
});
|
||||||
export type _HedProcessorConfig = Required<Pick<HedImageProcessorInvocation, 'id' | 'type' | 'scribble'>>;
|
|
||||||
export type HedProcessorConfig = z.infer<typeof zHedProcessorConfig>;
|
export type HedProcessorConfig = z.infer<typeof zHedProcessorConfig>;
|
||||||
|
|
||||||
const zLineartAnimeProcessorConfig = z.object({
|
const zLineartAnimeProcessorConfig = z.object({
|
||||||
id: zId,
|
id: zId,
|
||||||
type: z.literal('lineart_anime_image_processor'),
|
type: z.literal('lineart_anime_image_processor'),
|
||||||
});
|
});
|
||||||
export type _LineartAnimeProcessorConfig = Required<Pick<LineartAnimeImageProcessorInvocation, 'id' | 'type'>>;
|
|
||||||
export type LineartAnimeProcessorConfig = z.infer<typeof zLineartAnimeProcessorConfig>;
|
export type LineartAnimeProcessorConfig = z.infer<typeof zLineartAnimeProcessorConfig>;
|
||||||
|
|
||||||
const zLineartProcessorConfig = z.object({
|
const zLineartProcessorConfig = z.object({
|
||||||
@@ -93,7 +59,6 @@ const zLineartProcessorConfig = z.object({
|
|||||||
type: z.literal('lineart_image_processor'),
|
type: z.literal('lineart_image_processor'),
|
||||||
coarse: z.boolean(),
|
coarse: z.boolean(),
|
||||||
});
|
});
|
||||||
export type _LineartProcessorConfig = Required<Pick<LineartImageProcessorInvocation, 'id' | 'type' | 'coarse'>>;
|
|
||||||
export type LineartProcessorConfig = z.infer<typeof zLineartProcessorConfig>;
|
export type LineartProcessorConfig = z.infer<typeof zLineartProcessorConfig>;
|
||||||
|
|
||||||
const zMediapipeFaceProcessorConfig = z.object({
|
const zMediapipeFaceProcessorConfig = z.object({
|
||||||
@@ -102,9 +67,6 @@ const zMediapipeFaceProcessorConfig = z.object({
|
|||||||
max_faces: z.number().int().gte(1),
|
max_faces: z.number().int().gte(1),
|
||||||
min_confidence: z.number().gte(0).lte(1),
|
min_confidence: z.number().gte(0).lte(1),
|
||||||
});
|
});
|
||||||
export type _MediapipeFaceProcessorConfig = Required<
|
|
||||||
Pick<MediapipeFaceProcessorInvocation, 'id' | 'type' | 'max_faces' | 'min_confidence'>
|
|
||||||
>;
|
|
||||||
export type MediapipeFaceProcessorConfig = z.infer<typeof zMediapipeFaceProcessorConfig>;
|
export type MediapipeFaceProcessorConfig = z.infer<typeof zMediapipeFaceProcessorConfig>;
|
||||||
|
|
||||||
const zMidasDepthProcessorConfig = z.object({
|
const zMidasDepthProcessorConfig = z.object({
|
||||||
@@ -113,9 +75,6 @@ const zMidasDepthProcessorConfig = z.object({
|
|||||||
a_mult: z.number().gte(0),
|
a_mult: z.number().gte(0),
|
||||||
bg_th: z.number().gte(0),
|
bg_th: z.number().gte(0),
|
||||||
});
|
});
|
||||||
export type _MidasDepthProcessorConfig = Required<
|
|
||||||
Pick<MidasDepthImageProcessorInvocation, 'id' | 'type' | 'a_mult' | 'bg_th'>
|
|
||||||
>;
|
|
||||||
export type MidasDepthProcessorConfig = z.infer<typeof zMidasDepthProcessorConfig>;
|
export type MidasDepthProcessorConfig = z.infer<typeof zMidasDepthProcessorConfig>;
|
||||||
|
|
||||||
const zMlsdProcessorConfig = z.object({
|
const zMlsdProcessorConfig = z.object({
|
||||||
@@ -124,14 +83,12 @@ const zMlsdProcessorConfig = z.object({
|
|||||||
thr_v: z.number().gte(0),
|
thr_v: z.number().gte(0),
|
||||||
thr_d: z.number().gte(0),
|
thr_d: z.number().gte(0),
|
||||||
});
|
});
|
||||||
export type _MlsdProcessorConfig = Required<Pick<MlsdImageProcessorInvocation, 'id' | 'type' | 'thr_v' | 'thr_d'>>;
|
|
||||||
export type MlsdProcessorConfig = z.infer<typeof zMlsdProcessorConfig>;
|
export type MlsdProcessorConfig = z.infer<typeof zMlsdProcessorConfig>;
|
||||||
|
|
||||||
const zNormalbaeProcessorConfig = z.object({
|
const zNormalbaeProcessorConfig = z.object({
|
||||||
id: zId,
|
id: zId,
|
||||||
type: z.literal('normalbae_image_processor'),
|
type: z.literal('normalbae_image_processor'),
|
||||||
});
|
});
|
||||||
export type _NormalbaeProcessorConfig = Required<Pick<NormalbaeImageProcessorInvocation, 'id' | 'type'>>;
|
|
||||||
export type NormalbaeProcessorConfig = z.infer<typeof zNormalbaeProcessorConfig>;
|
export type NormalbaeProcessorConfig = z.infer<typeof zNormalbaeProcessorConfig>;
|
||||||
|
|
||||||
const zDWOpenposeProcessorConfig = z.object({
|
const zDWOpenposeProcessorConfig = z.object({
|
||||||
@@ -141,9 +98,6 @@ const zDWOpenposeProcessorConfig = z.object({
|
|||||||
draw_face: z.boolean(),
|
draw_face: z.boolean(),
|
||||||
draw_hands: z.boolean(),
|
draw_hands: z.boolean(),
|
||||||
});
|
});
|
||||||
export type _DWOpenposeProcessorConfig = Required<
|
|
||||||
Pick<DWOpenposeImageProcessorInvocation, 'id' | 'type' | 'draw_body' | 'draw_face' | 'draw_hands'>
|
|
||||||
>;
|
|
||||||
export type DWOpenposeProcessorConfig = z.infer<typeof zDWOpenposeProcessorConfig>;
|
export type DWOpenposeProcessorConfig = z.infer<typeof zDWOpenposeProcessorConfig>;
|
||||||
|
|
||||||
const zPidiProcessorConfig = z.object({
|
const zPidiProcessorConfig = z.object({
|
||||||
@@ -152,14 +106,12 @@ const zPidiProcessorConfig = z.object({
|
|||||||
safe: z.boolean(),
|
safe: z.boolean(),
|
||||||
scribble: z.boolean(),
|
scribble: z.boolean(),
|
||||||
});
|
});
|
||||||
export type _PidiProcessorConfig = Required<Pick<PidiImageProcessorInvocation, 'id' | 'type' | 'safe' | 'scribble'>>;
|
|
||||||
export type PidiProcessorConfig = z.infer<typeof zPidiProcessorConfig>;
|
export type PidiProcessorConfig = z.infer<typeof zPidiProcessorConfig>;
|
||||||
|
|
||||||
const zZoeDepthProcessorConfig = z.object({
|
const zZoeDepthProcessorConfig = z.object({
|
||||||
id: zId,
|
id: zId,
|
||||||
type: z.literal('zoe_depth_image_processor'),
|
type: z.literal('zoe_depth_image_processor'),
|
||||||
});
|
});
|
||||||
export type _ZoeDepthProcessorConfig = Required<Pick<ZoeDepthImageProcessorInvocation, 'id' | 'type'>>;
|
|
||||||
export type ZoeDepthProcessorConfig = z.infer<typeof zZoeDepthProcessorConfig>;
|
export type ZoeDepthProcessorConfig = z.infer<typeof zZoeDepthProcessorConfig>;
|
||||||
|
|
||||||
const zProcessorConfig = z.discriminatedUnion('type', [
|
const zProcessorConfig = z.discriminatedUnion('type', [
|
||||||
@@ -198,8 +150,8 @@ const zControlAdapterBase = z.object({
|
|||||||
weight: z.number().gte(0).lte(1),
|
weight: z.number().gte(0).lte(1),
|
||||||
image: zImageWithDims.nullable(),
|
image: zImageWithDims.nullable(),
|
||||||
processedImage: zImageWithDims.nullable(),
|
processedImage: zImageWithDims.nullable(),
|
||||||
isProcessingImage: z.boolean(),
|
|
||||||
processorConfig: zProcessorConfig.nullable(),
|
processorConfig: zProcessorConfig.nullable(),
|
||||||
|
processorPendingBatchId: z.string().nullable().default(null),
|
||||||
beginEndStepPct: zBeginEndStepPct,
|
beginEndStepPct: zBeginEndStepPct,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -521,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'> = {
|
||||||
@@ -532,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'> = {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -567,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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ export const LoRACard = memo((props: LoRACardProps) => {
|
|||||||
<CompositeNumberInput
|
<CompositeNumberInput
|
||||||
value={lora.weight}
|
value={lora.weight}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
min={-5}
|
min={-10}
|
||||||
max={5}
|
max={10}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
w={20}
|
w={20}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
|
|||||||
@@ -587,7 +587,7 @@ const parseControlNetToControlAdapterLayer: MetadataParseFunc<ControlAdapterLaye
|
|||||||
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
||||||
processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null,
|
processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null,
|
||||||
processorConfig,
|
processorConfig,
|
||||||
isProcessingImage: false,
|
processorPendingBatchId: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -651,7 +651,7 @@ const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc<ControlAdapterLaye
|
|||||||
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
||||||
processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null,
|
processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null,
|
||||||
processorConfig,
|
processorConfig,
|
||||||
isProcessingImage: false,
|
processorPendingBatchId: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,26 +2,34 @@ import 'reactflow/dist/style.css';
|
|||||||
|
|
||||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||||
import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library';
|
import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { useAppDispatch, useAppStore } from 'app/store/storeHooks';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import type { SelectInstance } from 'chakra-react-select';
|
import type { SelectInstance } from 'chakra-react-select';
|
||||||
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
|
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
|
||||||
import {
|
import {
|
||||||
addNodePopoverClosed,
|
$cursorPos,
|
||||||
addNodePopoverOpened,
|
$isAddNodePopoverOpen,
|
||||||
|
$pendingConnection,
|
||||||
|
$templates,
|
||||||
|
closeAddNodePopover,
|
||||||
|
connectionMade,
|
||||||
nodeAdded,
|
nodeAdded,
|
||||||
selectNodesSlice,
|
openAddNodePopover,
|
||||||
} from 'features/nodes/store/nodesSlice';
|
} from 'features/nodes/store/nodesSlice';
|
||||||
|
import { getFirstValidConnection } from 'features/nodes/store/util/findConnectionToValidHandle';
|
||||||
import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes';
|
import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes';
|
||||||
|
import type { AnyNode } from 'features/nodes/types/invocation';
|
||||||
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import { filter, map, memoize, some } from 'lodash-es';
|
import { filter, map, memoize, some } from 'lodash-es';
|
||||||
import type { KeyboardEventHandler } from 'react';
|
import type { KeyboardEventHandler } from 'react';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
|
import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters';
|
import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
const createRegex = memoize(
|
const createRegex = memoize(
|
||||||
(inputValue: string) =>
|
(inputValue: string) =>
|
||||||
@@ -54,26 +62,30 @@ const AddNodePopover = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const selectRef = useRef<SelectInstance<ComboboxOption> | null>(null);
|
const selectRef = useRef<SelectInstance<ComboboxOption> | null>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const templates = useStore($templates);
|
||||||
|
const pendingConnection = useStore($pendingConnection);
|
||||||
|
const isOpen = useStore($isAddNodePopoverOpen);
|
||||||
|
const store = useAppStore();
|
||||||
|
|
||||||
const fieldFilter = useAppSelector((s) => s.nodes.connectionStartFieldType);
|
const filteredTemplates = useMemo(() => {
|
||||||
const handleFilter = useAppSelector((s) => s.nodes.connectionStartParams?.handleType);
|
|
||||||
|
|
||||||
const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
|
|
||||||
// If we have a connection in progress, we need to filter the node choices
|
// If we have a connection in progress, we need to filter the node choices
|
||||||
const filteredNodeTemplates = fieldFilter
|
if (!pendingConnection) {
|
||||||
? filter(nodes.templates, (template) => {
|
return map(templates);
|
||||||
const handles = handleFilter === 'source' ? template.inputs : template.outputs;
|
}
|
||||||
|
|
||||||
return some(handles, (handle) => {
|
|
||||||
const sourceType = handleFilter === 'source' ? fieldFilter : handle.type;
|
|
||||||
const targetType = handleFilter === 'target' ? fieldFilter : handle.type;
|
|
||||||
|
|
||||||
|
return filter(templates, (template) => {
|
||||||
|
const pendingFieldKind = pendingConnection.fieldTemplate.fieldKind;
|
||||||
|
const fields = pendingFieldKind === 'input' ? template.outputs : template.inputs;
|
||||||
|
return some(fields, (field) => {
|
||||||
|
const sourceType = pendingFieldKind === 'input' ? field.type : pendingConnection.fieldTemplate.type;
|
||||||
|
const targetType = pendingFieldKind === 'output' ? field.type : pendingConnection.fieldTemplate.type;
|
||||||
return validateSourceAndTargetTypes(sourceType, targetType);
|
return validateSourceAndTargetTypes(sourceType, targetType);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
: map(nodes.templates);
|
}, [templates, pendingConnection]);
|
||||||
|
|
||||||
const options: ComboboxOption[] = map(filteredNodeTemplates, (template) => {
|
const options = useMemo(() => {
|
||||||
|
const _options: ComboboxOption[] = map(filteredTemplates, (template) => {
|
||||||
return {
|
return {
|
||||||
label: template.title,
|
label: template.title,
|
||||||
value: template.type,
|
value: template.type,
|
||||||
@@ -83,15 +95,15 @@ const AddNodePopover = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//We only want these nodes if we're not filtered
|
//We only want these nodes if we're not filtered
|
||||||
if (fieldFilter === null) {
|
if (!pendingConnection) {
|
||||||
options.push({
|
_options.push({
|
||||||
label: t('nodes.currentImage'),
|
label: t('nodes.currentImage'),
|
||||||
value: 'current_image',
|
value: 'current_image',
|
||||||
description: t('nodes.currentImageDescription'),
|
description: t('nodes.currentImageDescription'),
|
||||||
tags: ['progress'],
|
tags: ['progress'],
|
||||||
});
|
});
|
||||||
|
|
||||||
options.push({
|
_options.push({
|
||||||
label: t('nodes.notes'),
|
label: t('nodes.notes'),
|
||||||
value: 'notes',
|
value: 'notes',
|
||||||
description: t('nodes.notesDescription'),
|
description: t('nodes.notesDescription'),
|
||||||
@@ -99,18 +111,15 @@ const AddNodePopover = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
options.sort((a, b) => a.label.localeCompare(b.label));
|
_options.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
return { options };
|
return _options;
|
||||||
});
|
}, [filteredTemplates, pendingConnection, t]);
|
||||||
|
|
||||||
const { options } = useAppSelector(selector);
|
|
||||||
const isOpen = useAppSelector((s) => s.nodes.isAddNodePopoverOpen);
|
|
||||||
|
|
||||||
const addNode = useCallback(
|
const addNode = useCallback(
|
||||||
(nodeType: string) => {
|
(nodeType: string): AnyNode | null => {
|
||||||
const invocation = buildInvocation(nodeType);
|
const node = buildInvocation(nodeType);
|
||||||
if (!invocation) {
|
if (!node) {
|
||||||
const errorMessage = t('nodes.unknownNode', {
|
const errorMessage = t('nodes.unknownNode', {
|
||||||
nodeType: nodeType,
|
nodeType: nodeType,
|
||||||
});
|
});
|
||||||
@@ -118,10 +127,11 @@ const AddNodePopover = () => {
|
|||||||
status: 'error',
|
status: 'error',
|
||||||
title: errorMessage,
|
title: errorMessage,
|
||||||
});
|
});
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
const cursorPos = $cursorPos.get();
|
||||||
dispatch(nodeAdded(invocation));
|
dispatch(nodeAdded({ node, cursorPos }));
|
||||||
|
return node;
|
||||||
},
|
},
|
||||||
[dispatch, buildInvocation, toaster, t]
|
[dispatch, buildInvocation, toaster, t]
|
||||||
);
|
);
|
||||||
@@ -131,52 +141,50 @@ const AddNodePopover = () => {
|
|||||||
if (!v) {
|
if (!v) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addNode(v.value);
|
const node = addNode(v.value);
|
||||||
dispatch(addNodePopoverClosed());
|
|
||||||
|
// Auto-connect an edge if we just added a node and have a pending connection
|
||||||
|
if (pendingConnection && isInvocationNode(node)) {
|
||||||
|
const template = templates[node.data.type];
|
||||||
|
assert(template, 'Template not found');
|
||||||
|
const { nodes, edges } = store.getState().nodes.present;
|
||||||
|
const connection = getFirstValidConnection(templates, nodes, edges, pendingConnection, node, template);
|
||||||
|
if (connection) {
|
||||||
|
dispatch(connectionMade(connection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAddNodePopover();
|
||||||
},
|
},
|
||||||
[addNode, dispatch]
|
[addNode, dispatch, pendingConnection, store, templates]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const handleHotkeyOpen: HotkeyCallback = useCallback((e) => {
|
||||||
dispatch(addNodePopoverClosed());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const onOpen = useCallback(() => {
|
|
||||||
dispatch(addNodePopoverOpened());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const handleHotkeyOpen: HotkeyCallback = useCallback(
|
|
||||||
(e) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onOpen();
|
openAddNodePopover();
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
selectRef.current?.inputRef?.focus();
|
selectRef.current?.inputRef?.focus();
|
||||||
});
|
});
|
||||||
},
|
}, []);
|
||||||
[onOpen]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleHotkeyClose: HotkeyCallback = useCallback(() => {
|
const handleHotkeyClose: HotkeyCallback = useCallback(() => {
|
||||||
onClose();
|
closeAddNodePopover();
|
||||||
}, [onClose]);
|
}, []);
|
||||||
|
|
||||||
useHotkeys(['shift+a', 'space'], handleHotkeyOpen);
|
useHotkeys(['shift+a', 'space'], handleHotkeyOpen);
|
||||||
useHotkeys(['escape'], handleHotkeyClose);
|
useHotkeys(['escape'], handleHotkeyClose);
|
||||||
const onKeyDown: KeyboardEventHandler = useCallback(
|
const onKeyDown: KeyboardEventHandler = useCallback((e) => {
|
||||||
(e) => {
|
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
onClose();
|
closeAddNodePopover();
|
||||||
}
|
}
|
||||||
},
|
}, []);
|
||||||
[onClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]);
|
const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={closeAddNodePopover}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
openDelay={0}
|
openDelay={0}
|
||||||
closeDelay={0}
|
closeDelay={0}
|
||||||
@@ -206,7 +214,7 @@ const AddNodePopover = () => {
|
|||||||
noOptionsMessage={noOptionsMessage}
|
noOptionsMessage={noOptionsMessage}
|
||||||
filterOption={filterOption}
|
filterOption={filterOption}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onMenuClose={onClose}
|
onMenuClose={closeAddNodePopover}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
closeMenuOnSelect={false}
|
closeMenuOnSelect={false}
|
||||||
|
|||||||
@@ -1,34 +1,33 @@
|
|||||||
import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
|
import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useConnection } from 'features/nodes/hooks/useConnection';
|
||||||
|
import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste';
|
||||||
|
import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
|
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
|
||||||
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
|
||||||
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
|
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
|
||||||
import {
|
import {
|
||||||
connectionEnded,
|
$cursorPos,
|
||||||
|
$isAddNodePopoverOpen,
|
||||||
|
$isUpdatingEdge,
|
||||||
|
$pendingConnection,
|
||||||
|
$viewport,
|
||||||
connectionMade,
|
connectionMade,
|
||||||
connectionStarted,
|
|
||||||
edgeAdded,
|
edgeAdded,
|
||||||
edgeChangeStarted,
|
|
||||||
edgeDeleted,
|
edgeDeleted,
|
||||||
edgesChanged,
|
edgesChanged,
|
||||||
edgesDeleted,
|
edgesDeleted,
|
||||||
nodesChanged,
|
nodesChanged,
|
||||||
nodesDeleted,
|
nodesDeleted,
|
||||||
|
redo,
|
||||||
selectedAll,
|
selectedAll,
|
||||||
selectedEdgesChanged,
|
undo,
|
||||||
selectedNodesChanged,
|
|
||||||
selectionCopied,
|
|
||||||
selectionPasted,
|
|
||||||
viewportChanged,
|
|
||||||
} from 'features/nodes/store/nodesSlice';
|
} from 'features/nodes/store/nodesSlice';
|
||||||
import { $flow } from 'features/nodes/store/reactFlowInstance';
|
import { $flow } from 'features/nodes/store/reactFlowInstance';
|
||||||
import type { CSSProperties, MouseEvent } from 'react';
|
import type { CSSProperties, MouseEvent } from 'react';
|
||||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import type {
|
import type {
|
||||||
OnConnect,
|
|
||||||
OnConnectEnd,
|
|
||||||
OnConnectStart,
|
|
||||||
OnEdgesChange,
|
OnEdgesChange,
|
||||||
OnEdgesDelete,
|
OnEdgesDelete,
|
||||||
OnEdgeUpdateFunc,
|
OnEdgeUpdateFunc,
|
||||||
@@ -36,12 +35,11 @@ import type {
|
|||||||
OnMoveEnd,
|
OnMoveEnd,
|
||||||
OnNodesChange,
|
OnNodesChange,
|
||||||
OnNodesDelete,
|
OnNodesDelete,
|
||||||
OnSelectionChangeFunc,
|
|
||||||
ProOptions,
|
ProOptions,
|
||||||
ReactFlowProps,
|
ReactFlowProps,
|
||||||
XYPosition,
|
ReactFlowState,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import { Background, ReactFlow } from 'reactflow';
|
import { Background, ReactFlow, useStore as useReactFlowStore } from 'reactflow';
|
||||||
|
|
||||||
import CustomConnectionLine from './connectionLines/CustomConnectionLine';
|
import CustomConnectionLine from './connectionLines/CustomConnectionLine';
|
||||||
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
|
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
|
||||||
@@ -68,17 +66,23 @@ const proOptions: ProOptions = { hideAttribution: true };
|
|||||||
|
|
||||||
const snapGrid: [number, number] = [25, 25];
|
const snapGrid: [number, number] = [25, 25];
|
||||||
|
|
||||||
|
const selectCancelConnection = (state: ReactFlowState) => state.cancelConnection;
|
||||||
|
|
||||||
export const Flow = memo(() => {
|
export const Flow = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const nodes = useAppSelector((s) => s.nodes.nodes);
|
const nodes = useAppSelector((s) => s.nodes.present.nodes);
|
||||||
const edges = useAppSelector((s) => s.nodes.edges);
|
const edges = useAppSelector((s) => s.nodes.present.edges);
|
||||||
const viewport = useAppSelector((s) => s.nodes.viewport);
|
const viewport = useStore($viewport);
|
||||||
const shouldSnapToGrid = useAppSelector((s) => s.nodes.shouldSnapToGrid);
|
const mayUndo = useAppSelector((s) => s.nodes.past.length > 0);
|
||||||
const selectionMode = useAppSelector((s) => s.nodes.selectionMode);
|
const mayRedo = useAppSelector((s) => s.nodes.future.length > 0);
|
||||||
|
const shouldSnapToGrid = useAppSelector((s) => s.workflowSettings.shouldSnapToGrid);
|
||||||
|
const selectionMode = useAppSelector((s) => s.workflowSettings.selectionMode);
|
||||||
|
const { onConnectStart, onConnect, onConnectEnd } = useConnection();
|
||||||
const flowWrapper = useRef<HTMLDivElement>(null);
|
const flowWrapper = useRef<HTMLDivElement>(null);
|
||||||
const cursorPosition = useRef<XYPosition | null>(null);
|
|
||||||
const isValidConnection = useIsValidConnection();
|
const isValidConnection = useIsValidConnection();
|
||||||
|
const cancelConnection = useReactFlowStore(selectCancelConnection);
|
||||||
useWorkflowWatcher();
|
useWorkflowWatcher();
|
||||||
|
useSyncExecutionState();
|
||||||
const [borderRadius] = useToken('radii', ['base']);
|
const [borderRadius] = useToken('radii', ['base']);
|
||||||
|
|
||||||
const flowStyles = useMemo<CSSProperties>(
|
const flowStyles = useMemo<CSSProperties>(
|
||||||
@@ -102,32 +106,6 @@ export const Flow = memo(() => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onConnectStart: OnConnectStart = useCallback(
|
|
||||||
(event, params) => {
|
|
||||||
dispatch(connectionStarted(params));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onConnect: OnConnect = useCallback(
|
|
||||||
(connection) => {
|
|
||||||
dispatch(connectionMade(connection));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onConnectEnd: OnConnectEnd = useCallback(() => {
|
|
||||||
if (!cursorPosition.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(
|
|
||||||
connectionEnded({
|
|
||||||
cursorPosition: cursorPosition.current,
|
|
||||||
mouseOverNodeId: $mouseOverNode.get(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const onEdgesDelete: OnEdgesDelete = useCallback(
|
const onEdgesDelete: OnEdgesDelete = useCallback(
|
||||||
(edges) => {
|
(edges) => {
|
||||||
dispatch(edgesDeleted(edges));
|
dispatch(edgesDeleted(edges));
|
||||||
@@ -142,20 +120,9 @@ export const Flow = memo(() => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectionChange: OnSelectionChangeFunc = useCallback(
|
const handleMoveEnd: OnMoveEnd = useCallback((e, viewport) => {
|
||||||
({ nodes, edges }) => {
|
$viewport.set(viewport);
|
||||||
dispatch(selectedNodesChanged(nodes ? nodes.map((n) => n.id) : []));
|
}, []);
|
||||||
dispatch(selectedEdgesChanged(edges ? edges.map((e) => e.id) : []));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMoveEnd: OnMoveEnd = useCallback(
|
|
||||||
(e, viewport) => {
|
|
||||||
dispatch(viewportChanged(viewport));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { onCloseGlobal } = useGlobalMenuClose();
|
const { onCloseGlobal } = useGlobalMenuClose();
|
||||||
const handlePaneClick = useCallback(() => {
|
const handlePaneClick = useCallback(() => {
|
||||||
@@ -169,11 +136,12 @@ export const Flow = memo(() => {
|
|||||||
|
|
||||||
const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => {
|
const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => {
|
||||||
if (flowWrapper.current?.getBoundingClientRect()) {
|
if (flowWrapper.current?.getBoundingClientRect()) {
|
||||||
cursorPosition.current =
|
$cursorPos.set(
|
||||||
$flow.get()?.screenToFlowPosition({
|
$flow.get()?.screenToFlowPosition({
|
||||||
x: event.clientX,
|
x: event.clientX,
|
||||||
y: event.clientY,
|
y: event.clientY,
|
||||||
}) ?? null;
|
}) ?? null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -195,19 +163,18 @@ export const Flow = memo(() => {
|
|||||||
|
|
||||||
const onEdgeUpdateStart: NonNullable<ReactFlowProps['onEdgeUpdateStart']> = useCallback(
|
const onEdgeUpdateStart: NonNullable<ReactFlowProps['onEdgeUpdateStart']> = useCallback(
|
||||||
(e, edge, _handleType) => {
|
(e, edge, _handleType) => {
|
||||||
|
$isUpdatingEdge.set(true);
|
||||||
// update mouse event
|
// update mouse event
|
||||||
edgeUpdateMouseEvent.current = e;
|
edgeUpdateMouseEvent.current = e;
|
||||||
// always delete the edge when starting an updated
|
// always delete the edge when starting an updated
|
||||||
dispatch(edgeDeleted(edge.id));
|
dispatch(edgeDeleted(edge.id));
|
||||||
dispatch(edgeChangeStarted());
|
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onEdgeUpdate: OnEdgeUpdateFunc = useCallback(
|
const onEdgeUpdate: OnEdgeUpdateFunc = useCallback(
|
||||||
(_oldEdge, newConnection) => {
|
(_oldEdge, newConnection) => {
|
||||||
// instead of updating the edge (we deleted it earlier), we instead create
|
// Because we deleted the edge when the update started, we must create a new edge from the connection
|
||||||
// a new one.
|
|
||||||
dispatch(connectionMade(newConnection));
|
dispatch(connectionMade(newConnection));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
@@ -215,8 +182,10 @@ export const Flow = memo(() => {
|
|||||||
|
|
||||||
const onEdgeUpdateEnd: NonNullable<ReactFlowProps['onEdgeUpdateEnd']> = useCallback(
|
const onEdgeUpdateEnd: NonNullable<ReactFlowProps['onEdgeUpdateEnd']> = useCallback(
|
||||||
(e, edge, _handleType) => {
|
(e, edge, _handleType) => {
|
||||||
// Handle the case where user begins a drag but didn't move the cursor -
|
$isUpdatingEdge.set(false);
|
||||||
// bc we deleted the edge, we need to add it back
|
$pendingConnection.set(null);
|
||||||
|
// Handle the case where user begins a drag but didn't move the cursor - we deleted the edge when starting
|
||||||
|
// the edge update - we need to add it back
|
||||||
if (
|
if (
|
||||||
// ignore touch events
|
// ignore touch events
|
||||||
!('touches' in e) &&
|
!('touches' in e) &&
|
||||||
@@ -233,23 +202,64 @@ export const Flow = memo(() => {
|
|||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
useHotkeys(['Ctrl+c', 'Meta+c'], (e) => {
|
const { copySelection, pasteSelection } = useCopyPaste();
|
||||||
e.preventDefault();
|
|
||||||
dispatch(selectionCopied());
|
|
||||||
});
|
|
||||||
|
|
||||||
useHotkeys(['Ctrl+a', 'Meta+a'], (e) => {
|
const onCopyHotkey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
copySelection();
|
||||||
|
},
|
||||||
|
[copySelection]
|
||||||
|
);
|
||||||
|
useHotkeys(['Ctrl+c', 'Meta+c'], onCopyHotkey);
|
||||||
|
|
||||||
|
const onSelectAllHotkey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dispatch(selectedAll());
|
dispatch(selectedAll());
|
||||||
});
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
useHotkeys(['Ctrl+a', 'Meta+a'], onSelectAllHotkey);
|
||||||
|
|
||||||
useHotkeys(['Ctrl+v', 'Meta+v'], (e) => {
|
const onPasteHotkey = useCallback(
|
||||||
if (!cursorPosition.current) {
|
(e: KeyboardEvent) => {
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dispatch(selectionPasted({ cursorPosition: cursorPosition.current }));
|
pasteSelection();
|
||||||
});
|
},
|
||||||
|
[pasteSelection]
|
||||||
|
);
|
||||||
|
useHotkeys(['Ctrl+v', 'Meta+v'], onPasteHotkey);
|
||||||
|
|
||||||
|
const onPasteWithEdgesToNodesHotkey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
pasteSelection(true);
|
||||||
|
},
|
||||||
|
[pasteSelection]
|
||||||
|
);
|
||||||
|
useHotkeys(['Ctrl+shift+v', 'Meta+shift+v'], onPasteWithEdgesToNodesHotkey);
|
||||||
|
|
||||||
|
const onUndoHotkey = useCallback(() => {
|
||||||
|
if (mayUndo) {
|
||||||
|
dispatch(undo());
|
||||||
|
}
|
||||||
|
}, [dispatch, mayUndo]);
|
||||||
|
useHotkeys(['meta+z', 'ctrl+z'], onUndoHotkey);
|
||||||
|
|
||||||
|
const onRedoHotkey = useCallback(() => {
|
||||||
|
if (mayRedo) {
|
||||||
|
dispatch(redo());
|
||||||
|
}
|
||||||
|
}, [dispatch, mayRedo]);
|
||||||
|
useHotkeys(['meta+shift+z', 'ctrl+shift+z'], onRedoHotkey);
|
||||||
|
|
||||||
|
const onEscapeHotkey = useCallback(() => {
|
||||||
|
$pendingConnection.set(null);
|
||||||
|
$isAddNodePopoverOpen.set(false);
|
||||||
|
cancelConnection();
|
||||||
|
}, [cancelConnection]);
|
||||||
|
useHotkeys('esc', onEscapeHotkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
@@ -274,7 +284,6 @@ export const Flow = memo(() => {
|
|||||||
onConnectEnd={onConnectEnd}
|
onConnectEnd={onConnectEnd}
|
||||||
onMoveEnd={handleMoveEnd}
|
onMoveEnd={handleMoveEnd}
|
||||||
connectionLineComponent={CustomConnectionLine}
|
connectionLineComponent={CustomConnectionLine}
|
||||||
onSelectionChange={handleSelectionChange}
|
|
||||||
isValidConnection={isValidConnection}
|
isValidConnection={isValidConnection}
|
||||||
minZoom={0.1}
|
minZoom={0.1}
|
||||||
snapToGrid={shouldSnapToGrid}
|
snapToGrid={shouldSnapToGrid}
|
||||||
@@ -285,6 +294,7 @@ export const Flow = memo(() => {
|
|||||||
onPaneClick={handlePaneClick}
|
onPaneClick={handlePaneClick}
|
||||||
deleteKeyCode={DELETE_KEYS}
|
deleteKeyCode={DELETE_KEYS}
|
||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
|
elevateEdgesOnSelect
|
||||||
>
|
>
|
||||||
<Background />
|
<Background />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||||
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
|
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { $pendingConnection } from 'features/nodes/store/nodesSlice';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import type { ConnectionLineComponentProps } from 'reactflow';
|
import type { ConnectionLineComponentProps } from 'reactflow';
|
||||||
import { getBezierPath } from 'reactflow';
|
import { getBezierPath } from 'reactflow';
|
||||||
|
|
||||||
const selectStroke = createSelector(selectNodesSlice, (nodes) =>
|
|
||||||
nodes.shouldColorEdges ? getFieldColor(nodes.connectionStartFieldType) : colorTokenToCssVar('base.500')
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectClassName = createSelector(selectNodesSlice, (nodes) =>
|
|
||||||
nodes.shouldAnimateEdges ? 'react-flow__custom_connection-path animated' : 'react-flow__custom_connection-path'
|
|
||||||
);
|
|
||||||
|
|
||||||
const pathStyles: CSSProperties = { opacity: 0.8 };
|
const pathStyles: CSSProperties = { opacity: 0.8 };
|
||||||
|
|
||||||
const CustomConnectionLine = ({ fromX, fromY, fromPosition, toX, toY, toPosition }: ConnectionLineComponentProps) => {
|
const CustomConnectionLine = ({ fromX, fromY, fromPosition, toX, toY, toPosition }: ConnectionLineComponentProps) => {
|
||||||
const stroke = useAppSelector(selectStroke);
|
const pendingConnection = useStore($pendingConnection);
|
||||||
const className = useAppSelector(selectClassName);
|
const shouldColorEdges = useAppSelector((state) => state.workflowSettings.shouldColorEdges);
|
||||||
|
const shouldAnimateEdges = useAppSelector((state) => state.workflowSettings.shouldAnimateEdges);
|
||||||
|
const stroke = useMemo(() => {
|
||||||
|
if (shouldColorEdges && pendingConnection) {
|
||||||
|
return getFieldColor(pendingConnection.fieldTemplate.type);
|
||||||
|
} else {
|
||||||
|
return colorTokenToCssVar('base.500');
|
||||||
|
}
|
||||||
|
}, [pendingConnection, shouldColorEdges]);
|
||||||
|
const className = useMemo(() => {
|
||||||
|
if (shouldAnimateEdges) {
|
||||||
|
return 'react-flow__custom_connection-path animated';
|
||||||
|
} else {
|
||||||
|
return 'react-flow__custom_connection-path';
|
||||||
|
}
|
||||||
|
}, [shouldAnimateEdges]);
|
||||||
|
|
||||||
const pathParams = {
|
const pathParams = {
|
||||||
sourceX: fromX,
|
sourceX: fromX,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Badge, Flex } from '@invoke-ai/ui-library';
|
import { Badge, Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
|
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
|
||||||
|
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import type { EdgeProps } from 'reactflow';
|
import type { EdgeProps } from 'reactflow';
|
||||||
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow';
|
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow';
|
||||||
@@ -22,9 +24,10 @@ const InvocationCollapsedEdge = ({
|
|||||||
sourceHandleId,
|
sourceHandleId,
|
||||||
targetHandleId,
|
targetHandleId,
|
||||||
}: EdgeProps<{ count: number }>) => {
|
}: EdgeProps<{ count: number }>) => {
|
||||||
|
const templates = useStore($templates);
|
||||||
const selector = useMemo(
|
const selector = useMemo(
|
||||||
() => makeEdgeSelector(source, sourceHandleId, target, targetHandleId, selected),
|
() => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId, selected),
|
||||||
[selected, source, sourceHandleId, target, targetHandleId]
|
[templates, selected, source, sourceHandleId, target, targetHandleId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isSelected, shouldAnimate } = useAppSelector(selector);
|
const { isSelected, shouldAnimate } = useAppSelector(selector);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import type { EdgeProps } from 'reactflow';
|
import type { EdgeProps } from 'reactflow';
|
||||||
@@ -21,13 +23,14 @@ const InvocationDefaultEdge = ({
|
|||||||
sourceHandleId,
|
sourceHandleId,
|
||||||
targetHandleId,
|
targetHandleId,
|
||||||
}: EdgeProps) => {
|
}: EdgeProps) => {
|
||||||
|
const templates = useStore($templates);
|
||||||
const selector = useMemo(
|
const selector = useMemo(
|
||||||
() => makeEdgeSelector(source, sourceHandleId, target, targetHandleId, selected),
|
() => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId, selected),
|
||||||
[source, sourceHandleId, target, targetHandleId, selected]
|
[templates, source, sourceHandleId, target, targetHandleId, selected]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isSelected, shouldAnimate, stroke, label } = useAppSelector(selector);
|
const { isSelected, shouldAnimate, stroke, label } = useAppSelector(selector);
|
||||||
const shouldShowEdgeLabels = useAppSelector((s) => s.nodes.shouldShowEdgeLabels);
|
const shouldShowEdgeLabels = useAppSelector((s) => s.workflowSettings.shouldShowEdgeLabels);
|
||||||
|
|
||||||
const [edgePath, labelX, labelY] = getBezierPath({
|
const [edgePath, labelX, labelY] = getBezierPath({
|
||||||
sourceX,
|
sourceX,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
import { selectFieldOutputTemplate, selectNodeTemplate } from 'features/nodes/store/selectors';
|
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 { getFieldColor } from './getEdgeColor';
|
import { getFieldColor } from './getEdgeColor';
|
||||||
@@ -14,6 +15,7 @@ const defaultReturnValue = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const makeEdgeSelector = (
|
export const makeEdgeSelector = (
|
||||||
|
templates: Templates,
|
||||||
source: string,
|
source: string,
|
||||||
sourceHandleId: string | null | undefined,
|
sourceHandleId: string | null | undefined,
|
||||||
target: string,
|
target: string,
|
||||||
@@ -22,7 +24,8 @@ export const makeEdgeSelector = (
|
|||||||
) =>
|
) =>
|
||||||
createMemoizedSelector(
|
createMemoizedSelector(
|
||||||
selectNodesSlice,
|
selectNodesSlice,
|
||||||
(nodes): { isSelected: boolean; shouldAnimate: boolean; stroke: string; label: string } => {
|
selectWorkflowSettingsSlice,
|
||||||
|
(nodes, workflowSettings): { isSelected: boolean; shouldAnimate: boolean; stroke: string; label: string } => {
|
||||||
const sourceNode = nodes.nodes.find((node) => node.id === source);
|
const sourceNode = nodes.nodes.find((node) => node.id === source);
|
||||||
const targetNode = nodes.nodes.find((node) => node.id === target);
|
const targetNode = nodes.nodes.find((node) => node.id === target);
|
||||||
|
|
||||||
@@ -33,19 +36,20 @@ export const makeEdgeSelector = (
|
|||||||
return defaultReturnValue;
|
return defaultReturnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputFieldTemplate = selectFieldOutputTemplate(nodes, sourceNode.id, sourceHandleId);
|
const sourceNodeTemplate = templates[sourceNode.data.type];
|
||||||
|
const targetNodeTemplate = templates[targetNode.data.type];
|
||||||
|
|
||||||
|
const outputFieldTemplate = sourceNodeTemplate?.outputs[sourceHandleId];
|
||||||
const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined;
|
const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined;
|
||||||
|
|
||||||
const stroke = sourceType && nodes.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500');
|
const stroke =
|
||||||
|
sourceType && workflowSettings.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500');
|
||||||
const sourceNodeTemplate = selectNodeTemplate(nodes, sourceNode.id);
|
|
||||||
const targetNodeTemplate = selectNodeTemplate(nodes, targetNode.id);
|
|
||||||
|
|
||||||
const label = `${sourceNodeTemplate?.title || sourceNode.data?.label} -> ${targetNodeTemplate?.title || targetNode.data?.label}`;
|
const label = `${sourceNodeTemplate?.title || sourceNode.data?.label} -> ${targetNodeTemplate?.title || targetNode.data?.label}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSelected,
|
isSelected,
|
||||||
shouldAnimate: nodes.shouldAnimateEdges && isSelected,
|
shouldAnimate: workflowSettings.shouldAnimateEdges && isSelected,
|
||||||
stroke,
|
stroke,
|
||||||
label,
|
label,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
import { Badge, CircularProgress, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library';
|
import { Badge, CircularProgress, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||||
import type { NodeExecutionState } from 'features/nodes/types/invocation';
|
import type { NodeExecutionState } from 'features/nodes/types/invocation';
|
||||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCheckBold, PiDotsThreeOutlineFill, PiWarningBold } from 'react-icons/pi';
|
import { PiCheckBold, PiDotsThreeOutlineFill, PiWarningBold } from 'react-icons/pi';
|
||||||
|
|
||||||
@@ -24,12 +22,7 @@ const circleStyles: SystemStyleObject = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const InvocationNodeStatusIndicator = ({ nodeId }: Props) => {
|
const InvocationNodeStatusIndicator = ({ nodeId }: Props) => {
|
||||||
const selectNodeExecutionState = useMemo(
|
const nodeExecutionState = useExecutionState(nodeId);
|
||||||
() => createMemoizedSelector(selectNodesSlice, (nodes) => nodes.nodeExecutionStates[nodeId]),
|
|
||||||
[nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const nodeExecutionState = useAppSelector(selectNodeExecutionState);
|
|
||||||
|
|
||||||
if (!nodeExecutionState) {
|
if (!nodeExecutionState) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import InvocationNode from 'features/nodes/components/flow/nodes/Invocation/InvocationNode';
|
import InvocationNode from 'features/nodes/components/flow/nodes/Invocation/InvocationNode';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||||
import type { InvocationNodeData } from 'features/nodes/types/invocation';
|
import type { InvocationNodeData } from 'features/nodes/types/invocation';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import type { NodeProps } from 'reactflow';
|
import type { NodeProps } from 'reactflow';
|
||||||
@@ -11,13 +11,13 @@ import InvocationNodeUnknownFallback from './InvocationNodeUnknownFallback';
|
|||||||
const InvocationNodeWrapper = (props: NodeProps<InvocationNodeData>) => {
|
const InvocationNodeWrapper = (props: NodeProps<InvocationNodeData>) => {
|
||||||
const { data, selected } = props;
|
const { data, selected } = props;
|
||||||
const { id: nodeId, type, isOpen, label } = data;
|
const { id: nodeId, type, isOpen, label } = data;
|
||||||
|
const templates = useStore($templates);
|
||||||
|
const hasTemplate = useMemo(() => Boolean(templates[type]), [templates, type]);
|
||||||
|
const nodeExists = useAppSelector((s) => Boolean(s.nodes.present.nodes.find((n) => n.id === nodeId)));
|
||||||
|
|
||||||
const hasTemplateSelector = useMemo(
|
if (!nodeExists) {
|
||||||
() => createSelector(selectNodesSlice, (nodes) => Boolean(nodes.templates[type])),
|
return null;
|
||||||
[type]
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const hasTemplate = useAppSelector(hasTemplateSelector);
|
|
||||||
|
|
||||||
if (!hasTemplate) {
|
if (!hasTemplate) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
|||||||
const [localTitle, setLocalTitle] = useState(label || fieldTemplateTitle || t('nodes.unknownField'));
|
const [localTitle, setLocalTitle] = useState(label || fieldTemplateTitle || t('nodes.unknownField'));
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (newTitle: string) => {
|
async (newTitleRaw: string) => {
|
||||||
|
const newTitle = newTitleRaw.trim();
|
||||||
if (newTitle && (newTitle === label || newTitle === fieldTemplateTitle)) {
|
if (newTitle && (newTitle === label || newTitle === fieldTemplateTitle)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -57,10 +58,6 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
|||||||
}, [label, fieldTemplateTitle, t]);
|
}, [label, fieldTemplateTitle, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
|
||||||
label={withTooltip ? <FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="inputs" /> : undefined}
|
|
||||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
|
||||||
>
|
|
||||||
<Editable
|
<Editable
|
||||||
value={localTitle}
|
value={localTitle}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@@ -73,6 +70,10 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
|||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
gap={1}
|
gap={1}
|
||||||
w="full"
|
w="full"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
label={withTooltip ? <FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="inputs" /> : undefined}
|
||||||
|
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||||
>
|
>
|
||||||
<EditablePreview
|
<EditablePreview
|
||||||
fontWeight="semibold"
|
fontWeight="semibold"
|
||||||
@@ -80,10 +81,10 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
|||||||
noOfLines={1}
|
noOfLines={1}
|
||||||
color={isMissingInput ? 'error.300' : 'base.300'}
|
color={isMissingInput ? 'error.300' : 'base.300'}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
<EditableInput className="nodrag" sx={editableInputStyles} />
|
<EditableInput className="nodrag" sx={editableInputStyles} />
|
||||||
<EditableControls />
|
<EditableControls />
|
||||||
</Editable>
|
</Editable>
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,7 +128,15 @@ const EditableControls = memo(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex onClick={handleClick} position="absolute" w="full" h="full" top={0} insetInlineStart={0} cursor="text" />
|
<Flex
|
||||||
|
onClick={handleClick}
|
||||||
|
position="absolute"
|
||||||
|
w="min-content"
|
||||||
|
h="full"
|
||||||
|
top={0}
|
||||||
|
insetInlineStart={0}
|
||||||
|
cursor="text"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldTemplate.input === 'connection') {
|
if (fieldTemplate.input === 'connection' || isConnected) {
|
||||||
return (
|
return (
|
||||||
<InputFieldWrapper shouldDim={shouldDim}>
|
<InputFieldWrapper shouldDim={shouldDim}>
|
||||||
<FormControl isInvalid={isMissingInput} isDisabled={isConnected} px={2}>
|
<FormControl isInvalid={isMissingInput} isDisabled={isConnected} px={2}>
|
||||||
@@ -95,7 +95,15 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<InputFieldWrapper shouldDim={shouldDim}>
|
<InputFieldWrapper shouldDim={shouldDim}>
|
||||||
<FormControl isInvalid={isMissingInput} isDisabled={isConnected} orientation="vertical" px={2}>
|
<FormControl
|
||||||
|
isInvalid={isMissingInput}
|
||||||
|
isDisabled={isConnected}
|
||||||
|
// Without pointerEvents prop, disabled inputs don't trigger reactflow events. For example, when making a
|
||||||
|
// connection, the mouse up to end the connection won't fire, leaving the connection in-progress.
|
||||||
|
pointerEvents={isConnected ? 'none' : 'auto'}
|
||||||
|
orientation="vertical"
|
||||||
|
px={2}
|
||||||
|
>
|
||||||
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||||
<Flex>
|
<Flex>
|
||||||
<EditableFieldTitle
|
<EditableFieldTitle
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||||
import { Box, useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
|
import { Box, useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
|
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
|
||||||
|
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||||
import { nodeExclusivelySelected, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { nodeExclusivelySelected } from 'features/nodes/store/nodesSlice';
|
||||||
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from 'features/nodes/types/constants';
|
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from 'features/nodes/types/constants';
|
||||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
import type { MouseEvent, PropsWithChildren } from 'react';
|
import type { MouseEvent, PropsWithChildren } from 'react';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
type NodeWrapperProps = PropsWithChildren & {
|
type NodeWrapperProps = PropsWithChildren & {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -20,16 +20,8 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
|||||||
const { nodeId, width, children, selected } = props;
|
const { nodeId, width, children, selected } = props;
|
||||||
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
|
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
|
||||||
|
|
||||||
const selectIsInProgress = useMemo(
|
const executionState = useExecutionState(nodeId);
|
||||||
() =>
|
const isInProgress = executionState?.status === zNodeStatus.enum.IN_PROGRESS;
|
||||||
createSelector(
|
|
||||||
selectNodesSlice,
|
|
||||||
(nodes) => nodes.nodeExecutionStates[nodeId]?.status === zNodeStatus.enum.IN_PROGRESS
|
|
||||||
),
|
|
||||||
[nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isInProgress = useAppSelector(selectIsInProgress);
|
|
||||||
|
|
||||||
const [nodeInProgress, shadowsXl, shadowsBase] = useToken('shadows', [
|
const [nodeInProgress, shadowsXl, shadowsBase] = useToken('shadows', [
|
||||||
'nodeInProgress',
|
'nodeInProgress',
|
||||||
@@ -39,7 +31,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const opacity = useAppSelector((s) => s.nodes.nodeOpacity);
|
const opacity = useAppSelector((s) => s.workflowSettings.nodeOpacity);
|
||||||
const { onCloseGlobal } = useGlobalMenuClose();
|
const { onCloseGlobal } = useGlobalMenuClose();
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { CompositeSlider, Flex } from '@invoke-ai/ui-library';
|
import { CompositeSlider, Flex } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { nodeOpacityChanged } from 'features/nodes/store/nodesSlice';
|
import { nodeOpacityChanged } from 'features/nodes/store/workflowSettingsSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const NodeOpacitySlider = () => {
|
const NodeOpacitySlider = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const nodeOpacity = useAppSelector((s) => s.nodes.nodeOpacity);
|
const nodeOpacity = useAppSelector((s) => s.workflowSettings.nodeOpacity);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import { shouldShowMinimapPanelChanged } from 'features/nodes/store/workflowSettingsSlice';
|
||||||
// shouldShowFieldTypeLegendChanged,
|
|
||||||
shouldShowMinimapPanelChanged,
|
|
||||||
} from 'features/nodes/store/nodesSlice';
|
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
@@ -19,9 +16,9 @@ const ViewportControls = () => {
|
|||||||
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
// const shouldShowFieldTypeLegend = useAppSelector(
|
// const shouldShowFieldTypeLegend = useAppSelector(
|
||||||
// (s) => s.nodes.shouldShowFieldTypeLegend
|
// (s) => s.nodes.present.shouldShowFieldTypeLegend
|
||||||
// );
|
// );
|
||||||
const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.shouldShowMinimapPanel);
|
const shouldShowMinimapPanel = useAppSelector((s) => s.workflowSettings.shouldShowMinimapPanel);
|
||||||
|
|
||||||
const handleClickedZoomIn = useCallback(() => {
|
const handleClickedZoomIn = useCallback(() => {
|
||||||
zoomIn();
|
zoomIn();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const minimapStyles: SystemStyleObject = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MinimapPanel = () => {
|
const MinimapPanel = () => {
|
||||||
const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.shouldShowMinimapPanel);
|
const shouldShowMinimapPanel = useAppSelector((s) => s.workflowSettings.shouldShowMinimapPanel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} position="absolute" bottom={0} insetInlineEnd={0}>
|
<Flex gap={2} position="absolute" bottom={0} insetInlineEnd={0}>
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { openAddNodePopover } from 'features/nodes/store/nodesSlice';
|
||||||
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
import { memo } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
const AddNodeButton = () => {
|
const AddNodeButton = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleOpenAddNodePopover = useCallback(() => {
|
|
||||||
dispatch(addNodePopoverOpened());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={t('nodes.addNodeToolTip')}
|
tooltip={t('nodes.addNodeToolTip')}
|
||||||
aria-label={t('nodes.addNode')}
|
aria-label={t('nodes.addNode')}
|
||||||
icon={<PiPlusBold />}
|
icon={<PiPlusBold />}
|
||||||
onClick={handleOpenAddNodePopover}
|
onClick={openAddNodePopover}
|
||||||
pointerEvents="auto"
|
pointerEvents="auto"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopRightPanel/ReloadSchemaButton';
|
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopRightPanel/ReloadSchemaButton';
|
||||||
import {
|
import {
|
||||||
selectionModeChanged,
|
selectionModeChanged,
|
||||||
selectNodesSlice,
|
selectWorkflowSettingsSlice,
|
||||||
shouldAnimateEdgesChanged,
|
shouldAnimateEdgesChanged,
|
||||||
shouldColorEdgesChanged,
|
shouldColorEdgesChanged,
|
||||||
shouldShowEdgeLabelsChanged,
|
shouldShowEdgeLabelsChanged,
|
||||||
shouldSnapToGridChanged,
|
shouldSnapToGridChanged,
|
||||||
shouldValidateGraphChanged,
|
shouldValidateGraphChanged,
|
||||||
} from 'features/nodes/store/nodesSlice';
|
} from 'features/nodes/store/workflowSettingsSlice';
|
||||||
import type { ChangeEvent, ReactNode } from 'react';
|
import type { ChangeEvent, ReactNode } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -35,7 +35,7 @@ import { SelectionMode } from 'reactflow';
|
|||||||
|
|
||||||
const formLabelProps: FormLabelProps = { flexGrow: 1 };
|
const formLabelProps: FormLabelProps = { flexGrow: 1 };
|
||||||
|
|
||||||
const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
|
const selector = createMemoizedSelector(selectWorkflowSettingsSlice, (workflowSettings) => {
|
||||||
const {
|
const {
|
||||||
shouldAnimateEdges,
|
shouldAnimateEdges,
|
||||||
shouldValidateGraph,
|
shouldValidateGraph,
|
||||||
@@ -43,7 +43,7 @@ const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
|
|||||||
shouldColorEdges,
|
shouldColorEdges,
|
||||||
shouldShowEdgeLabels,
|
shouldShowEdgeLabels,
|
||||||
selectionMode,
|
selectionMode,
|
||||||
} = nodes;
|
} = workflowSettings;
|
||||||
return {
|
return {
|
||||||
shouldAnimateEdges,
|
shouldAnimateEdges,
|
||||||
shouldValidateGraph,
|
shouldValidateGraph,
|
||||||
|
|||||||
@@ -3,27 +3,21 @@ import { useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { selectLastSelectedNode } from 'features/nodes/store/selectors';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
|
|
||||||
const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1];
|
|
||||||
|
|
||||||
const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId);
|
const selector = createMemoizedSelector(selectNodesSlice, (nodes) => selectLastSelectedNode(nodes));
|
||||||
|
|
||||||
return {
|
|
||||||
data: lastSelectedNode?.data,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const InspectorDataTab = () => {
|
const InspectorDataTab = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data } = useAppSelector(selector);
|
const lastSelectedNode = useAppSelector(selector);
|
||||||
|
|
||||||
if (!data) {
|
if (!lastSelectedNode) {
|
||||||
return <IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />;
|
return <IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <DataViewer data={data} label="Node Data" />;
|
return <DataViewer data={lastSelectedNode.data} label="Node Data" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(InspectorDataTab);
|
export default memo(InspectorDataTab);
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import { Box, Flex, FormControl, FormLabel, HStack, Text } from '@invoke-ai/ui-library';
|
import { Box, Flex, FormControl, FormLabel, HStack, Text } from '@invoke-ai/ui-library';
|
||||||
|
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 { 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 NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
|
import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
|
||||||
import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
|
import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { selectLastSelectedNode } from 'features/nodes/store/selectors';
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import { memo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import EditableNodeTitle from './details/EditableNodeTitle';
|
import EditableNodeTitle from './details/EditableNodeTitle';
|
||||||
|
|
||||||
const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
|
const InspectorDetailsTab = () => {
|
||||||
const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1];
|
const templates = useStore($templates);
|
||||||
|
const selector = useMemo(
|
||||||
const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId);
|
() =>
|
||||||
|
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
||||||
const lastSelectedNodeTemplate = lastSelectedNode ? nodes.templates[lastSelectedNode.data.type] : undefined;
|
const lastSelectedNode = selectLastSelectedNode(nodes);
|
||||||
|
const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined;
|
||||||
|
|
||||||
if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) {
|
if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) {
|
||||||
return;
|
return;
|
||||||
@@ -28,9 +31,9 @@ const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
|
|||||||
nodeVersion: lastSelectedNode.data.version,
|
nodeVersion: lastSelectedNode.data.version,
|
||||||
templateTitle: lastSelectedNodeTemplate.title,
|
templateTitle: lastSelectedNodeTemplate.title,
|
||||||
};
|
};
|
||||||
});
|
}),
|
||||||
|
[templates]
|
||||||
const InspectorDetailsTab = () => {
|
);
|
||||||
const data = useAppSelector(selector);
|
const data = useAppSelector(selector);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,49 @@
|
|||||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||||
|
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 { 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 DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
|
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { selectLastSelectedNode } from 'features/nodes/store/selectors';
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import { memo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { ImageOutput } from 'services/api/types';
|
import type { ImageOutput } from 'services/api/types';
|
||||||
import type { AnyResult } from 'services/events/types';
|
import type { AnyResult } from 'services/events/types';
|
||||||
|
|
||||||
import ImageOutputPreview from './outputs/ImageOutputPreview';
|
import ImageOutputPreview from './outputs/ImageOutputPreview';
|
||||||
|
|
||||||
const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
|
const InspectorOutputsTab = () => {
|
||||||
const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1];
|
const templates = useStore($templates);
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
||||||
|
const lastSelectedNode = selectLastSelectedNode(nodes);
|
||||||
|
const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined;
|
||||||
|
|
||||||
const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId);
|
if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) {
|
||||||
|
|
||||||
const lastSelectedNodeTemplate = lastSelectedNode ? nodes.templates[lastSelectedNode.data.type] : undefined;
|
|
||||||
|
|
||||||
const nes = nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__'];
|
|
||||||
|
|
||||||
if (!isInvocationNode(lastSelectedNode) || !nes || !lastSelectedNodeTemplate) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
outputs: nes.outputs,
|
nodeId: lastSelectedNode.id,
|
||||||
outputType: lastSelectedNodeTemplate.outputType,
|
outputType: lastSelectedNodeTemplate.outputType,
|
||||||
};
|
};
|
||||||
});
|
}),
|
||||||
|
[templates]
|
||||||
const InspectorOutputsTab = () => {
|
);
|
||||||
const data = useAppSelector(selector);
|
const data = useAppSelector(selector);
|
||||||
|
const nes = useExecutionState(data?.nodeId);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!data) {
|
if (!data || !nes) {
|
||||||
return <IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />;
|
return <IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.outputs.length === 0) {
|
if (nes.outputs.length === 0) {
|
||||||
return <IAINoContentFallback label={t('nodes.noOutputRecorded')} icon={null} />;
|
return <IAINoContentFallback label={t('nodes.noOutputRecorded')} icon={null} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,11 +52,11 @@ const InspectorOutputsTab = () => {
|
|||||||
<ScrollableContent>
|
<ScrollableContent>
|
||||||
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
|
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
|
||||||
{data.outputType === 'image_output' ? (
|
{data.outputType === 'image_output' ? (
|
||||||
data.outputs.map((result, i) => (
|
nes.outputs.map((result, i) => (
|
||||||
<ImageOutputPreview key={getKey(result, i)} output={result as ImageOutput} />
|
<ImageOutputPreview key={getKey(result, i)} output={result as ImageOutput} />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<DataViewer data={data.outputs} label={t('nodes.nodeOutputs')} />
|
<DataViewer data={nes.outputs} label={t('nodes.nodeOutputs')} />
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</ScrollableContent>
|
</ScrollableContent>
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
|
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 { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
import { memo } from 'react';
|
import { selectLastSelectedNode } from 'features/nodes/store/selectors';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
|
|
||||||
const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1];
|
|
||||||
|
|
||||||
const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId);
|
|
||||||
|
|
||||||
const lastSelectedNodeTemplate = lastSelectedNode ? nodes.templates[lastSelectedNode.data.type] : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
template: lastSelectedNodeTemplate,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const NodeTemplateInspector = () => {
|
const NodeTemplateInspector = () => {
|
||||||
const { template } = useAppSelector(selector);
|
const templates = useStore($templates);
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
||||||
|
const lastSelectedNode = selectLastSelectedNode(nodes);
|
||||||
|
const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined;
|
||||||
|
|
||||||
|
return lastSelectedNodeTemplate;
|
||||||
|
}),
|
||||||
|
[templates]
|
||||||
|
);
|
||||||
|
const template = useAppSelector(selector);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
|
||||||
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 { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
import { selectNodeTemplate } from 'features/nodes/store/selectors';
|
|
||||||
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
|
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
|
||||||
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
|
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
|
||||||
import { keys, map } from 'lodash-es';
|
import { keys, map } from 'lodash-es';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => {
|
export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => {
|
||||||
const selector = useMemo(
|
const template = useNodeTemplate(nodeId);
|
||||||
|
const selectConnectedFieldNames = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
createMemoizedSelector(selectNodesSlice, (nodesSlice) =>
|
||||||
const template = selectNodeTemplate(nodes, nodeId);
|
nodesSlice.edges
|
||||||
if (!template) {
|
.filter((e) => e.target === nodeId)
|
||||||
return EMPTY_ARRAY;
|
.map((e) => e.targetHandle)
|
||||||
|
.filter(Boolean)
|
||||||
|
),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
const connectedFieldNames = useAppSelector(selectConnectedFieldNames);
|
||||||
|
|
||||||
|
const fieldNames = useMemo(() => {
|
||||||
|
const fields = map(template.inputs).filter((field) => {
|
||||||
|
if (connectedFieldNames.includes(field.name)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
const fields = map(template.inputs).filter(
|
|
||||||
(field) =>
|
return (
|
||||||
(['any', 'direct'].includes(field.input) || field.type.isCollectionOrScalar) &&
|
(['any', 'direct'].includes(field.input) || field.type.isCollectionOrScalar) &&
|
||||||
keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
|
keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
|
||||||
);
|
);
|
||||||
|
});
|
||||||
return getSortedFilteredFieldNames(fields);
|
return getSortedFilteredFieldNames(fields);
|
||||||
}),
|
}, [connectedFieldNames, template.inputs]);
|
||||||
[nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fieldNames = useAppSelector(selector);
|
|
||||||
return fieldNames;
|
return fieldNames;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||||
import { NODE_WIDTH } from 'features/nodes/types/constants';
|
import { NODE_WIDTH } from 'features/nodes/types/constants';
|
||||||
import type { AnyNode, InvocationTemplate } from 'features/nodes/types/invocation';
|
import type { AnyNode, InvocationTemplate } from 'features/nodes/types/invocation';
|
||||||
import { buildCurrentImageNode } from 'features/nodes/util/node/buildCurrentImageNode';
|
import { buildCurrentImageNode } from 'features/nodes/util/node/buildCurrentImageNode';
|
||||||
@@ -8,8 +9,7 @@ import { useCallback } from 'react';
|
|||||||
import { useReactFlow } from 'reactflow';
|
import { useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
export const useBuildNode = () => {
|
export const useBuildNode = () => {
|
||||||
const nodeTemplates = useAppSelector((s) => s.nodes.templates);
|
const templates = useStore($templates);
|
||||||
|
|
||||||
const flow = useReactFlow();
|
const flow = useReactFlow();
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
@@ -41,10 +41,10 @@ export const useBuildNode = () => {
|
|||||||
|
|
||||||
// TODO: Keep track of invocation types so we do not need to cast this
|
// TODO: Keep track of invocation types so we do not need to cast this
|
||||||
// We know it is safe because the caller of this function gets the `type` arg from the list of invocation templates.
|
// We know it is safe because the caller of this function gets the `type` arg from the list of invocation templates.
|
||||||
const template = nodeTemplates[type] as InvocationTemplate;
|
const template = templates[type] as InvocationTemplate;
|
||||||
|
|
||||||
return buildInvocationNode(position, template);
|
return buildInvocationNode(position, template);
|
||||||
},
|
},
|
||||||
[nodeTemplates, flow]
|
[templates, flow]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useAppStore } from 'app/store/storeHooks';
|
||||||
|
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||||
|
import {
|
||||||
|
$isAddNodePopoverOpen,
|
||||||
|
$isUpdatingEdge,
|
||||||
|
$pendingConnection,
|
||||||
|
$templates,
|
||||||
|
connectionMade,
|
||||||
|
} from 'features/nodes/store/nodesSlice';
|
||||||
|
import { getFirstValidConnection } from 'features/nodes/store/util/findConnectionToValidHandle';
|
||||||
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import type { OnConnect, OnConnectEnd, OnConnectStart } from 'reactflow';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
export const useConnection = () => {
|
||||||
|
const store = useAppStore();
|
||||||
|
const templates = useStore($templates);
|
||||||
|
|
||||||
|
const onConnectStart = useCallback<OnConnectStart>(
|
||||||
|
(event, params) => {
|
||||||
|
const nodes = store.getState().nodes.present.nodes;
|
||||||
|
const { nodeId, handleId, handleType } = params;
|
||||||
|
assert(nodeId && handleId && handleType, `Invalid connection start params: ${JSON.stringify(params)}`);
|
||||||
|
const node = nodes.find((n) => n.id === nodeId);
|
||||||
|
assert(isInvocationNode(node), `Invalid node during connection: ${JSON.stringify(node)}`);
|
||||||
|
const template = templates[node.data.type];
|
||||||
|
assert(template, `Template not found for node type: ${node.data.type}`);
|
||||||
|
const fieldTemplate = handleType === 'source' ? template.outputs[handleId] : template.inputs[handleId];
|
||||||
|
assert(fieldTemplate, `Field template not found for field: ${node.data.type}.${handleId}`);
|
||||||
|
$pendingConnection.set({
|
||||||
|
node,
|
||||||
|
template,
|
||||||
|
fieldTemplate,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[store, templates]
|
||||||
|
);
|
||||||
|
const onConnect = useCallback<OnConnect>(
|
||||||
|
(connection) => {
|
||||||
|
const { dispatch } = store;
|
||||||
|
dispatch(connectionMade(connection));
|
||||||
|
$pendingConnection.set(null);
|
||||||
|
},
|
||||||
|
[store]
|
||||||
|
);
|
||||||
|
const onConnectEnd = useCallback<OnConnectEnd>(() => {
|
||||||
|
const { dispatch } = store;
|
||||||
|
const pendingConnection = $pendingConnection.get();
|
||||||
|
const isUpdatingEdge = $isUpdatingEdge.get();
|
||||||
|
const mouseOverNodeId = $mouseOverNode.get();
|
||||||
|
|
||||||
|
// If we are in the middle of an edge update, and the mouse isn't over a node, we should just bail so the edge
|
||||||
|
// update logic can finish up
|
||||||
|
if (isUpdatingEdge && !mouseOverNodeId) {
|
||||||
|
$pendingConnection.set(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pendingConnection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { nodes, edges } = store.getState().nodes.present;
|
||||||
|
if (mouseOverNodeId) {
|
||||||
|
const candidateNode = nodes.filter(isInvocationNode).find((n) => n.id === mouseOverNodeId);
|
||||||
|
if (!candidateNode) {
|
||||||
|
// The mouse is over a non-invocation node - bail
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const candidateTemplate = templates[candidateNode.data.type];
|
||||||
|
assert(candidateTemplate, `Template not found for node type: ${candidateNode.data.type}`);
|
||||||
|
const connection = getFirstValidConnection(
|
||||||
|
templates,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
pendingConnection,
|
||||||
|
candidateNode,
|
||||||
|
candidateTemplate
|
||||||
|
);
|
||||||
|
if (connection) {
|
||||||
|
dispatch(connectionMade(connection));
|
||||||
|
}
|
||||||
|
$pendingConnection.set(null);
|
||||||
|
} else {
|
||||||
|
// The mouse is not over a node - we should open the add node popover
|
||||||
|
$isAddNodePopoverOpen.set(true);
|
||||||
|
}
|
||||||
|
}, [store, templates]);
|
||||||
|
|
||||||
|
const api = useMemo(() => ({ onConnectStart, onConnect, onConnectEnd }), [onConnectStart, onConnect, onConnectEnd]);
|
||||||
|
return api;
|
||||||
|
};
|
||||||
@@ -1,34 +1,39 @@
|
|||||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
|
||||||
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 { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
import { selectNodeTemplate } from 'features/nodes/store/selectors';
|
|
||||||
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
|
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
|
||||||
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
|
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
|
||||||
import { keys, map } from 'lodash-es';
|
import { keys, map } from 'lodash-es';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useConnectionInputFieldNames = (nodeId: string): string[] => {
|
export const useConnectionInputFieldNames = (nodeId: string): string[] => {
|
||||||
const selector = useMemo(
|
const template = useNodeTemplate(nodeId);
|
||||||
|
const selectConnectedFieldNames = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
createMemoizedSelector(selectNodesSlice, (nodesSlice) =>
|
||||||
const template = selectNodeTemplate(nodes, nodeId);
|
nodesSlice.edges
|
||||||
if (!template) {
|
.filter((e) => e.target === nodeId)
|
||||||
return EMPTY_ARRAY;
|
.map((e) => e.targetHandle)
|
||||||
}
|
.filter(Boolean)
|
||||||
|
),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
const connectedFieldNames = useAppSelector(selectConnectedFieldNames);
|
||||||
|
|
||||||
|
const fieldNames = useMemo(() => {
|
||||||
// get the visible fields
|
// get the visible fields
|
||||||
const fields = map(template.inputs).filter(
|
const fields = map(template.inputs).filter((field) => {
|
||||||
(field) =>
|
if (connectedFieldNames.includes(field.name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
(field.input === 'connection' && !field.type.isCollectionOrScalar) ||
|
(field.input === 'connection' && !field.type.isCollectionOrScalar) ||
|
||||||
!keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
|
!keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return getSortedFilteredFieldNames(fields);
|
return getSortedFilteredFieldNames(fields);
|
||||||
}),
|
}, [connectedFieldNames, template.inputs]);
|
||||||
[nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fieldNames = useAppSelector(selector);
|
|
||||||
return fieldNames;
|
return fieldNames;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { $pendingConnection, $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector';
|
import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useFieldType } from './useFieldType.ts';
|
import { useFieldType } from './useFieldType.ts';
|
||||||
|
|
||||||
const selectIsConnectionInProgress = createSelector(
|
|
||||||
selectNodesSlice,
|
|
||||||
(nodes) => nodes.connectionStartFieldType !== null && nodes.connectionStartParams !== null
|
|
||||||
);
|
|
||||||
|
|
||||||
type UseConnectionStateProps = {
|
type UseConnectionStateProps = {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
@@ -18,6 +14,8 @@ type UseConnectionStateProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionStateProps) => {
|
export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionStateProps) => {
|
||||||
|
const pendingConnection = useStore($pendingConnection);
|
||||||
|
const templates = useStore($templates);
|
||||||
const fieldType = useFieldType(nodeId, fieldName, kind);
|
const fieldType = useFieldType(nodeId, fieldName, kind);
|
||||||
|
|
||||||
const selectIsConnected = useMemo(
|
const selectIsConnected = useMemo(
|
||||||
@@ -36,25 +34,30 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectConnectionError = useMemo(
|
const selectConnectionError = useMemo(
|
||||||
() => makeConnectionErrorSelector(nodeId, fieldName, kind === 'inputs' ? 'target' : 'source', fieldType),
|
|
||||||
[nodeId, fieldName, kind, fieldType]
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectIsConnectionStartField = useMemo(
|
|
||||||
() =>
|
() =>
|
||||||
createSelector(selectNodesSlice, (nodes) =>
|
makeConnectionErrorSelector(
|
||||||
Boolean(
|
templates,
|
||||||
nodes.connectionStartParams?.nodeId === nodeId &&
|
pendingConnection,
|
||||||
nodes.connectionStartParams?.handleId === fieldName &&
|
nodeId,
|
||||||
nodes.connectionStartParams?.handleType === { inputs: 'target', outputs: 'source' }[kind]
|
fieldName,
|
||||||
)
|
kind === 'inputs' ? 'target' : 'source',
|
||||||
|
fieldType
|
||||||
),
|
),
|
||||||
[fieldName, kind, nodeId]
|
[templates, pendingConnection, nodeId, fieldName, kind, fieldType]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isConnected = useAppSelector(selectIsConnected);
|
const isConnected = useAppSelector(selectIsConnected);
|
||||||
const isConnectionInProgress = useAppSelector(selectIsConnectionInProgress);
|
const isConnectionInProgress = useMemo(() => Boolean(pendingConnection), [pendingConnection]);
|
||||||
const isConnectionStartField = useAppSelector(selectIsConnectionStartField);
|
const isConnectionStartField = useMemo(() => {
|
||||||
|
if (!pendingConnection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
pendingConnection.node.id === nodeId &&
|
||||||
|
pendingConnection.fieldTemplate.name === fieldName &&
|
||||||
|
pendingConnection.fieldTemplate.fieldKind === { inputs: 'input', outputs: 'output' }[kind]
|
||||||
|
);
|
||||||
|
}, [fieldName, kind, nodeId, pendingConnection]);
|
||||||
const connectionError = useAppSelector(selectConnectionError);
|
const connectionError = useAppSelector(selectConnectionError);
|
||||||
|
|
||||||
const shouldDim = useMemo(
|
const shouldDim = useMemo(
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
|
import {
|
||||||
|
$copiedEdges,
|
||||||
|
$copiedNodes,
|
||||||
|
$cursorPos,
|
||||||
|
$edgesToCopiedNodes,
|
||||||
|
selectionPasted,
|
||||||
|
selectNodesSlice,
|
||||||
|
} from 'features/nodes/store/nodesSlice';
|
||||||
|
import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
|
||||||
|
import { isEqual, uniqWith } from 'lodash-es';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const copySelection = () => {
|
||||||
|
// Use the imperative API here so we don't have to pass the whole slice around
|
||||||
|
const { getState } = getStore();
|
||||||
|
const { nodes, edges } = selectNodesSlice(getState());
|
||||||
|
const selectedNodes = nodes.filter((node) => node.selected);
|
||||||
|
const selectedEdges = edges.filter((edge) => edge.selected);
|
||||||
|
const edgesToSelectedNodes = edges.filter((edge) => selectedNodes.some((node) => node.id === edge.target));
|
||||||
|
$copiedNodes.set(selectedNodes);
|
||||||
|
$copiedEdges.set(selectedEdges);
|
||||||
|
$edgesToCopiedNodes.set(edgesToSelectedNodes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pasteSelection = (withEdgesToCopiedNodes?: boolean) => {
|
||||||
|
const { getState, dispatch } = getStore();
|
||||||
|
const currentNodes = selectNodesSlice(getState()).nodes;
|
||||||
|
const cursorPos = $cursorPos.get();
|
||||||
|
|
||||||
|
const copiedNodes = deepClone($copiedNodes.get());
|
||||||
|
let copiedEdges = deepClone($copiedEdges.get());
|
||||||
|
|
||||||
|
if (withEdgesToCopiedNodes) {
|
||||||
|
const edgesToCopiedNodes = deepClone($edgesToCopiedNodes.get());
|
||||||
|
copiedEdges = uniqWith([...copiedEdges, ...edgesToCopiedNodes], isEqual);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate an offset to reposition nodes to surround the cursor position, maintaining relative positioning
|
||||||
|
const xCoords = copiedNodes.map((node) => node.position.x);
|
||||||
|
const yCoords = copiedNodes.map((node) => node.position.y);
|
||||||
|
const minX = Math.min(...xCoords);
|
||||||
|
const minY = Math.min(...yCoords);
|
||||||
|
const offsetX = cursorPos ? cursorPos.x - minX : 50;
|
||||||
|
const offsetY = cursorPos ? cursorPos.y - minY : 50;
|
||||||
|
|
||||||
|
copiedNodes.forEach((node) => {
|
||||||
|
const { x, y } = findUnoccupiedPosition(currentNodes, node.position.x + offsetX, node.position.y + offsetY);
|
||||||
|
node.position.x = x;
|
||||||
|
node.position.y = y;
|
||||||
|
// Pasted nodes are selected
|
||||||
|
node.selected = true;
|
||||||
|
// Also give em a fresh id
|
||||||
|
const id = uuidv4();
|
||||||
|
// Update the edges to point to the new node id
|
||||||
|
for (const edge of copiedEdges) {
|
||||||
|
if (edge.source === node.id) {
|
||||||
|
edge.source = id;
|
||||||
|
edge.id = edge.id.replace(node.data.id, id);
|
||||||
|
}
|
||||||
|
if (edge.target === node.id) {
|
||||||
|
edge.target = id;
|
||||||
|
edge.id = edge.id.replace(node.data.id, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.id = id;
|
||||||
|
node.data.id = id;
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(selectionPasted({ nodes: copiedNodes, edges: copiedEdges }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = { copySelection, pasteSelection };
|
||||||
|
|
||||||
|
export const useCopyPaste = () => {
|
||||||
|
return api;
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
|
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
|
import type { NodeExecutionStates } from 'features/nodes/store/types';
|
||||||
|
import type { NodeExecutionState } from 'features/nodes/types/invocation';
|
||||||
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
|
import { map } from 'nanostores';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
export const $nodeExecutionStates = map<NodeExecutionStates>({});
|
||||||
|
|
||||||
|
const initialNodeExecutionState: Omit<NodeExecutionState, 'nodeId'> = {
|
||||||
|
status: zNodeStatus.enum.PENDING,
|
||||||
|
error: null,
|
||||||
|
progress: null,
|
||||||
|
progressImage: null,
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExecutionState = (nodeId?: string) => {
|
||||||
|
const executionStates = useStore($nodeExecutionStates, nodeId ? { keys: [nodeId] } : undefined);
|
||||||
|
const executionState = useMemo(() => (nodeId ? executionStates[nodeId] : undefined), [executionStates, nodeId]);
|
||||||
|
return executionState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNodeExecutionState = (nodeId: string) => {
|
||||||
|
$nodeExecutionStates.setKey(nodeId, undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upsertExecutionState = (nodeId: string, updates?: Partial<NodeExecutionState>) => {
|
||||||
|
const state = $nodeExecutionStates.get()[nodeId];
|
||||||
|
if (!state) {
|
||||||
|
$nodeExecutionStates.setKey(nodeId, { ...deepClone(initialNodeExecutionState), nodeId, ...updates });
|
||||||
|
} else {
|
||||||
|
$nodeExecutionStates.setKey(nodeId, { ...state, ...updates });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectNodeIds = createMemoizedSelector(selectNodesSlice, (nodesSlice) => nodesSlice.nodes.map((node) => node.id));
|
||||||
|
|
||||||
|
export const useSyncExecutionState = () => {
|
||||||
|
const nodeIds = useAppSelector(selectNodeIds);
|
||||||
|
useEffect(() => {
|
||||||
|
const nodeExecutionStates = $nodeExecutionStates.get();
|
||||||
|
const nodeIdsToAdd = nodeIds.filter((id) => !nodeExecutionStates[id]);
|
||||||
|
const nodeIdsToRemove = Object.keys(nodeExecutionStates).filter((id) => !nodeIds.includes(id));
|
||||||
|
for (const id of nodeIdsToAdd) {
|
||||||
|
upsertExecutionState(id);
|
||||||
|
}
|
||||||
|
for (const id of nodeIdsToRemove) {
|
||||||
|
removeNodeExecutionState(id);
|
||||||
|
}
|
||||||
|
}, [nodeIds]);
|
||||||
|
};
|
||||||
@@ -1,20 +1,9 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { selectFieldInputTemplate } from 'features/nodes/store/selectors';
|
|
||||||
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate | null => {
|
export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate | null => {
|
||||||
const selector = useMemo(
|
const template = useNodeTemplate(nodeId);
|
||||||
() =>
|
const fieldTemplate = useMemo(() => template.inputs[fieldName] ?? null, [fieldName, template.inputs]);
|
||||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
|
||||||
return selectFieldInputTemplate(nodes, nodeId, fieldName);
|
|
||||||
}),
|
|
||||||
[fieldName, nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fieldTemplate = useAppSelector(selector);
|
|
||||||
|
|
||||||
return fieldTemplate;
|
return fieldTemplate;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { selectFieldOutputTemplate } from 'features/nodes/store/selectors';
|
|
||||||
import type { FieldOutputTemplate } from 'features/nodes/types/field';
|
import type { FieldOutputTemplate } from 'features/nodes/types/field';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useFieldOutputTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate | null => {
|
export const useFieldOutputTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate | null => {
|
||||||
const selector = useMemo(
|
const template = useNodeTemplate(nodeId);
|
||||||
() =>
|
const fieldTemplate = useMemo(() => template.outputs[fieldName] ?? null, [fieldName, template.outputs]);
|
||||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
|
||||||
return selectFieldOutputTemplate(nodes, nodeId, fieldName);
|
|
||||||
}),
|
|
||||||
[fieldName, nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fieldTemplate = useAppSelector(selector);
|
|
||||||
|
|
||||||
return fieldTemplate;
|
return fieldTemplate;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors';
|
import { selectInvocationNodeType } from 'features/nodes/store/selectors';
|
||||||
import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field';
|
import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
export const useFieldTemplate = (
|
export const useFieldTemplate = (
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
kind: 'inputs' | 'outputs'
|
kind: 'inputs' | 'outputs'
|
||||||
): FieldInputTemplate | FieldOutputTemplate | null => {
|
): FieldInputTemplate | FieldOutputTemplate => {
|
||||||
const selector = useMemo(
|
const templates = useStore($templates);
|
||||||
() =>
|
const selectNodeType = useMemo(
|
||||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
() => createSelector(selectNodesSlice, (nodes) => selectInvocationNodeType(nodes, nodeId)),
|
||||||
if (kind === 'inputs') {
|
[nodeId]
|
||||||
return selectFieldInputTemplate(nodes, nodeId, fieldName);
|
|
||||||
}
|
|
||||||
return selectFieldOutputTemplate(nodes, nodeId, fieldName);
|
|
||||||
}),
|
|
||||||
[fieldName, kind, nodeId]
|
|
||||||
);
|
);
|
||||||
|
const nodeType = useAppSelector(selectNodeType);
|
||||||
const fieldTemplate = useAppSelector(selector);
|
const fieldTemplate = useMemo(() => {
|
||||||
|
const template = templates[nodeType];
|
||||||
|
assert(template, `Template for node type ${nodeType} not found`);
|
||||||
|
if (kind === 'inputs') {
|
||||||
|
const fieldTemplate = template.inputs[fieldName];
|
||||||
|
assert(fieldTemplate, `Field template for field ${fieldName} not found`);
|
||||||
|
return fieldTemplate;
|
||||||
|
} else {
|
||||||
|
const fieldTemplate = template.outputs[fieldName];
|
||||||
|
assert(fieldTemplate, `Field template for field ${fieldName} not found`);
|
||||||
|
return fieldTemplate;
|
||||||
|
}
|
||||||
|
}, [fieldName, kind, nodeType, templates]);
|
||||||
|
|
||||||
return fieldTemplate;
|
return fieldTemplate;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useFieldTemplateTitle = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): string | null => {
|
export const useFieldTemplateTitle = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): string | null => {
|
||||||
const selector = useMemo(
|
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
|
||||||
() =>
|
const fieldTemplateTitle = useMemo(() => fieldTemplate.title, [fieldTemplate]);
|
||||||
createSelector(selectNodesSlice, (nodes) => {
|
|
||||||
if (kind === 'inputs') {
|
|
||||||
return selectFieldInputTemplate(nodes, nodeId, fieldName)?.title ?? null;
|
|
||||||
}
|
|
||||||
return selectFieldOutputTemplate(nodes, nodeId, fieldName)?.title ?? null;
|
|
||||||
}),
|
|
||||||
[fieldName, kind, nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fieldTemplateTitle = useAppSelector(selector);
|
|
||||||
|
|
||||||
return fieldTemplateTitle;
|
return fieldTemplateTitle;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors';
|
|
||||||
import type { FieldType } from 'features/nodes/types/field';
|
import type { FieldType } from 'features/nodes/types/field';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useFieldType = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): FieldType | null => {
|
export const useFieldType = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): FieldType => {
|
||||||
const selector = useMemo(
|
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
|
||||||
() =>
|
const fieldType = useMemo(() => fieldTemplate.type, [fieldTemplate]);
|
||||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
|
||||||
if (kind === 'inputs') {
|
|
||||||
return selectFieldInputTemplate(nodes, nodeId, fieldName)?.type ?? null;
|
|
||||||
}
|
|
||||||
return selectFieldOutputTemplate(nodes, nodeId, fieldName)?.type ?? null;
|
|
||||||
}),
|
|
||||||
[fieldName, kind, nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fieldType = useAppSelector(selector);
|
|
||||||
|
|
||||||
return fieldType;
|
return fieldType;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
|
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
const selector = createSelector(selectNodesSlice, (nodes) =>
|
export const useGetNodesNeedUpdate = () => {
|
||||||
|
const templates = useStore($templates);
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectNodesSlice, (nodes) =>
|
||||||
nodes.nodes.filter(isInvocationNode).some((node) => {
|
nodes.nodes.filter(isInvocationNode).some((node) => {
|
||||||
const template = nodes.templates[node.data.type];
|
const template = templates[node.data.type];
|
||||||
if (!template) {
|
if (!template) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return getNeedsUpdate(node, template);
|
return getNeedsUpdate(node.data, template);
|
||||||
})
|
})
|
||||||
|
),
|
||||||
|
[templates]
|
||||||
);
|
);
|
||||||
|
const needsUpdate = useAppSelector(selector);
|
||||||
export const useGetNodesNeedUpdate = () => {
|
return needsUpdate;
|
||||||
const getNeedsUpdate = useAppSelector(selector);
|
|
||||||
return getNeedsUpdate;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { selectNodeTemplate } from 'features/nodes/store/selectors';
|
|
||||||
import { some } from 'lodash-es';
|
import { some } from 'lodash-es';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useHasImageOutput = (nodeId: string): boolean => {
|
export const useHasImageOutput = (nodeId: string): boolean => {
|
||||||
const selector = useMemo(
|
const template = useNodeTemplate(nodeId);
|
||||||
|
const hasImageOutput = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
some(
|
||||||
const template = selectNodeTemplate(nodes, nodeId);
|
|
||||||
return some(
|
|
||||||
template?.outputs,
|
template?.outputs,
|
||||||
(output) =>
|
(output) =>
|
||||||
output.type.name === 'ImageField' &&
|
output.type.name === 'ImageField' &&
|
||||||
// the image primitive node (node type "image") does not actually save the image, do not show the image-saving checkboxes
|
// the image primitive node (node type "image") does not actually save the image, do not show the image-saving checkboxes
|
||||||
template?.type !== 'image'
|
template?.type !== 'image'
|
||||||
);
|
),
|
||||||
}),
|
[template]
|
||||||
[nodeId]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasImageOutput = useAppSelector(selector);
|
|
||||||
return hasImageOutput;
|
return hasImageOutput;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
// TODO: enable this at some point
|
// TODO: enable this at some point
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||||
|
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||||
import { getIsGraphAcyclic } from 'features/nodes/store/util/getIsGraphAcyclic';
|
import { getIsGraphAcyclic } from 'features/nodes/store/util/getIsGraphAcyclic';
|
||||||
|
import { getCollectItemType } from 'features/nodes/store/util/makeIsConnectionValidSelector';
|
||||||
import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes';
|
import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes';
|
||||||
import type { InvocationNodeData } from 'features/nodes/types/invocation';
|
import type { InvocationNodeData } from 'features/nodes/types/invocation';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import type { Connection, Node } from 'reactflow';
|
import type { Connection, Node } from 'reactflow';
|
||||||
|
|
||||||
@@ -13,7 +17,8 @@ import type { Connection, Node } from 'reactflow';
|
|||||||
|
|
||||||
export const useIsValidConnection = () => {
|
export const useIsValidConnection = () => {
|
||||||
const store = useAppStore();
|
const store = useAppStore();
|
||||||
const shouldValidateGraph = useAppSelector((s) => s.nodes.shouldValidateGraph);
|
const templates = useStore($templates);
|
||||||
|
const shouldValidateGraph = useAppSelector((s) => s.workflowSettings.shouldValidateGraph);
|
||||||
const isValidConnection = useCallback(
|
const isValidConnection = useCallback(
|
||||||
({ source, sourceHandle, target, targetHandle }: Connection): boolean => {
|
({ source, sourceHandle, target, targetHandle }: Connection): boolean => {
|
||||||
// Connection must have valid targets
|
// Connection must have valid targets
|
||||||
@@ -27,7 +32,7 @@ export const useIsValidConnection = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const { nodes, edges, templates } = state.nodes;
|
const { nodes, edges } = state.nodes.present;
|
||||||
|
|
||||||
// Find the source and target nodes
|
// Find the source and target nodes
|
||||||
const sourceNode = nodes.find((node) => node.id === source) as Node<InvocationNodeData>;
|
const sourceNode = nodes.find((node) => node.id === source) as Node<InvocationNodeData>;
|
||||||
@@ -40,6 +45,10 @@ export const useIsValidConnection = () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetFieldTemplate.input === 'direct') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!shouldValidateGraph) {
|
if (!shouldValidateGraph) {
|
||||||
// manual override!
|
// manual override!
|
||||||
return true;
|
return true;
|
||||||
@@ -57,6 +66,14 @@ export const useIsValidConnection = () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetNode.data.type === 'collect' && targetFieldTemplate.name === 'item') {
|
||||||
|
// Collect nodes shouldn't mix and match field types
|
||||||
|
const collectItemType = getCollectItemType(templates, nodes, edges, targetNode.id);
|
||||||
|
if (collectItemType) {
|
||||||
|
return isEqual(sourceFieldTemplate.type, collectItemType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Connection is invalid if target already has a connection
|
// Connection is invalid if target already has a connection
|
||||||
if (
|
if (
|
||||||
edges.find((edge) => {
|
edges.find((edge) => {
|
||||||
@@ -76,7 +93,7 @@ export const useIsValidConnection = () => {
|
|||||||
// Graphs much be acyclic (no loops!)
|
// Graphs much be acyclic (no loops!)
|
||||||
return getIsGraphAcyclic(source, target, nodes, edges);
|
return getIsGraphAcyclic(source, target, nodes, edges);
|
||||||
},
|
},
|
||||||
[shouldValidateGraph, store]
|
[shouldValidateGraph, templates, store]
|
||||||
);
|
);
|
||||||
|
|
||||||
return isValidConnection;
|
return isValidConnection;
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { selectNodeTemplate } from 'features/nodes/store/selectors';
|
|
||||||
import type { Classification } from 'features/nodes/types/common';
|
import type { Classification } from 'features/nodes/types/common';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useNodeClassification = (nodeId: string): Classification | null => {
|
export const useNodeClassification = (nodeId: string): Classification => {
|
||||||
const selector = useMemo(
|
const template = useNodeTemplate(nodeId);
|
||||||
() =>
|
const classification = useMemo(() => template.classification, [template]);
|
||||||
createSelector(selectNodesSlice, (nodes) => {
|
return classification;
|
||||||
return selectNodeTemplate(nodes, nodeId)?.classification ?? null;
|
|
||||||
}),
|
|
||||||
[nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const title = useAppSelector(selector);
|
|
||||||
return title;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { selectNodeData } from 'features/nodes/store/selectors';
|
|||||||
import type { InvocationNodeData } from 'features/nodes/types/invocation';
|
import type { InvocationNodeData } from 'features/nodes/types/invocation';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useNodeData = (nodeId: string): InvocationNodeData | null => {
|
export const useNodeData = (nodeId: string): InvocationNodeData => {
|
||||||
const selector = useMemo(
|
const selector = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
||||||
|
|||||||
@@ -1,25 +1,11 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { useNodeData } from 'features/nodes/hooks/useNodeData';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { selectInvocationNode, selectNodeTemplate } from 'features/nodes/store/selectors';
|
|
||||||
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
|
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useNodeNeedsUpdate = (nodeId: string) => {
|
export const useNodeNeedsUpdate = (nodeId: string) => {
|
||||||
const selector = useMemo(
|
const data = useNodeData(nodeId);
|
||||||
() =>
|
const template = useNodeTemplate(nodeId);
|
||||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
const needsUpdate = useMemo(() => getNeedsUpdate(data, template), [data, template]);
|
||||||
const node = selectInvocationNode(nodes, nodeId);
|
|
||||||
const template = selectNodeTemplate(nodes, nodeId);
|
|
||||||
if (!node || !template) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return getNeedsUpdate(node, template);
|
|
||||||
}),
|
|
||||||
[nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const needsUpdate = useAppSelector(selector);
|
|
||||||
|
|
||||||
return needsUpdate;
|
return needsUpdate;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
import { selectNodeTemplate } from 'features/nodes/store/selectors';
|
import { selectInvocationNodeType } from 'features/nodes/store/selectors';
|
||||||
import type { InvocationTemplate } from 'features/nodes/types/invocation';
|
import type { InvocationTemplate } from 'features/nodes/types/invocation';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
export const useNodeTemplate = (nodeId: string): InvocationTemplate | null => {
|
export const useNodeTemplate = (nodeId: string): InvocationTemplate => {
|
||||||
const selector = useMemo(
|
const templates = useStore($templates);
|
||||||
() =>
|
const selectNodeType = useMemo(
|
||||||
createSelector(selectNodesSlice, (nodes) => {
|
() => createSelector(selectNodesSlice, (nodes) => selectInvocationNodeType(nodes, nodeId)),
|
||||||
return selectNodeTemplate(nodes, nodeId);
|
|
||||||
}),
|
|
||||||
[nodeId]
|
[nodeId]
|
||||||
);
|
);
|
||||||
|
const nodeType = useAppSelector(selectNodeType);
|
||||||
const nodeTemplate = useAppSelector(selector);
|
const template = useMemo(() => {
|
||||||
|
const t = templates[nodeType];
|
||||||
return nodeTemplate;
|
assert(t, `Template for node type ${nodeType} not found`);
|
||||||
|
return t;
|
||||||
|
}, [nodeType, templates]);
|
||||||
|
return template;
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user