From dc5007fe951579f9890a37cc49c18554e71f828e Mon Sep 17 00:00:00 2001 From: Jonathan <34005131+JPPhoto@users.noreply.github.com> Date: Sun, 15 Mar 2026 09:04:15 -0500 Subject: [PATCH 01/56] Fix/model cache Qwen/CogView4 cancel repair (#8959) * Repair partially loaded Qwen models after cancel to avoid device mismatches * ruff * Repair CogView4 text encoder after canceled partial loads * Avoid MPS CI crash in repair regression test * Fix MPS device assertion in repair test --- .../app/invocations/cogview4_text_encoder.py | 15 +++- .../invocations/flux2_klein_text_encoder.py | 8 +- .../app/invocations/z_image_text_encoder.py | 14 +++- .../backend/model_manager/load/load_base.py | 10 +++ .../cached_model_with_partial_load.py | 21 +++++ .../invocations/test_cogview4_text_encoder.py | 80 +++++++++++++++++++ .../test_repair_required_tensors.py | 47 +++++++++++ 7 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 tests/app/invocations/test_cogview4_text_encoder.py create mode 100644 tests/backend/model_manager/load/model_cache/cached_model/test_repair_required_tensors.py diff --git a/invokeai/app/invocations/cogview4_text_encoder.py b/invokeai/app/invocations/cogview4_text_encoder.py index c6ef1663cf..3b5b1dc73f 100644 --- a/invokeai/app/invocations/cogview4_text_encoder.py +++ b/invokeai/app/invocations/cogview4_text_encoder.py @@ -6,6 +6,7 @@ from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField from invokeai.app.invocations.model import GlmEncoderField from invokeai.app.invocations.primitives import CogView4ConditioningOutput from invokeai.app.services.shared.invocation_context import InvocationContext +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( CogView4ConditioningInfo, ConditioningFieldData, @@ -46,10 +47,18 @@ class CogView4TextEncoderInvocation(BaseInvocation): prompt = [self.prompt] # TODO(ryand): Add model inputs to the invocation rather than hard-coding. + glm_text_encoder_info = context.models.load(self.glm_encoder.text_encoder) with ( - context.models.load(self.glm_encoder.text_encoder).model_on_device() as (_, glm_text_encoder), + glm_text_encoder_info.model_on_device() as (_, glm_text_encoder), context.models.load(self.glm_encoder.tokenizer).model_on_device() as (_, glm_tokenizer), ): + repaired_tensors = glm_text_encoder_info.repair_required_tensors_on_device() + device = get_effective_device(glm_text_encoder) + if repaired_tensors > 0: + context.logger.warning( + f"Recovered {repaired_tensors} required GLM tensor(s) onto {device} after a partial device mismatch." + ) + context.util.signal_progress("Running GLM text encoder") assert isinstance(glm_text_encoder, GlmModel) assert isinstance(glm_tokenizer, PreTrainedTokenizerFast) @@ -85,9 +94,7 @@ class CogView4TextEncoderInvocation(BaseInvocation): device=text_input_ids.device, ) text_input_ids = torch.cat([pad_ids, text_input_ids], dim=1) - prompt_embeds = glm_text_encoder( - text_input_ids.to(glm_text_encoder.device), output_hidden_states=True - ).hidden_states[-2] + prompt_embeds = glm_text_encoder(text_input_ids.to(device), output_hidden_states=True).hidden_states[-2] assert isinstance(prompt_embeds, torch.Tensor) return prompt_embeds diff --git a/invokeai/app/invocations/flux2_klein_text_encoder.py b/invokeai/app/invocations/flux2_klein_text_encoder.py index 6ca307ebf0..b44e782c8a 100644 --- a/invokeai/app/invocations/flux2_klein_text_encoder.py +++ b/invokeai/app/invocations/flux2_klein_text_encoder.py @@ -25,6 +25,7 @@ from invokeai.app.invocations.fields import ( from invokeai.app.invocations.model import Qwen3EncoderField from invokeai.app.invocations.primitives import FluxConditioningOutput from invokeai.app.services.shared.invocation_context import InvocationContext +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device from invokeai.backend.patches.layer_patcher import LayerPatcher from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_T5_PREFIX from invokeai.backend.patches.model_patch_raw import ModelPatchRaw @@ -100,7 +101,12 @@ class Flux2KleinTextEncoderInvocation(BaseInvocation): tokenizer_info = context.models.load(self.qwen3_encoder.tokenizer) (_, tokenizer) = exit_stack.enter_context(tokenizer_info.model_on_device()) - device = text_encoder.device + repaired_tensors = text_encoder_info.repair_required_tensors_on_device() + device = get_effective_device(text_encoder) + if repaired_tensors > 0: + context.logger.warning( + f"Recovered {repaired_tensors} required Qwen3 tensor(s) onto {device} after a partial device mismatch." + ) # Apply LoRA models lora_dtype = TorchDevice.choose_bfloat16_safe_dtype(device) diff --git a/invokeai/app/invocations/z_image_text_encoder.py b/invokeai/app/invocations/z_image_text_encoder.py index 06718c4897..c3405d6dc8 100644 --- a/invokeai/app/invocations/z_image_text_encoder.py +++ b/invokeai/app/invocations/z_image_text_encoder.py @@ -16,6 +16,7 @@ from invokeai.app.invocations.fields import ( from invokeai.app.invocations.model import Qwen3EncoderField from invokeai.app.invocations.primitives import ZImageConditioningOutput from invokeai.app.services.shared.invocation_context import InvocationContext +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device from invokeai.backend.patches.layer_patcher import LayerPatcher from invokeai.backend.patches.lora_conversions.z_image_lora_constants import Z_IMAGE_LORA_QWEN3_PREFIX from invokeai.backend.patches.model_patch_raw import ModelPatchRaw @@ -76,11 +77,17 @@ class ZImageTextEncoderInvocation(BaseInvocation): tokenizer_info = context.models.load(self.qwen3_encoder.tokenizer) with ExitStack() as exit_stack: - (_, text_encoder) = exit_stack.enter_context(text_encoder_info.model_on_device()) + (cached_weights, text_encoder) = exit_stack.enter_context(text_encoder_info.model_on_device()) (_, tokenizer) = exit_stack.enter_context(tokenizer_info.model_on_device()) - # Use the device that the text_encoder is actually on - device = text_encoder.device + # Use the device that the text encoder is effectively executing on, and repair any required tensors left on + # the CPU by a previous interrupted run. + repaired_tensors = text_encoder_info.repair_required_tensors_on_device() + device = get_effective_device(text_encoder) + if repaired_tensors > 0: + context.logger.warning( + f"Recovered {repaired_tensors} required Qwen3 tensor(s) onto {device} after a partial device mismatch." + ) # Apply LoRA models to the text encoder lora_dtype = TorchDevice.choose_bfloat16_safe_dtype(device) @@ -90,6 +97,7 @@ class ZImageTextEncoderInvocation(BaseInvocation): patches=self._lora_iterator(context), prefix=Z_IMAGE_LORA_QWEN3_PREFIX, dtype=lora_dtype, + cached_weights=cached_weights, ) ) diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index a4004afba7..b972969a68 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -14,6 +14,9 @@ import torch from invokeai.app.services.config import InvokeAIAppConfig from invokeai.backend.model_manager.configs.factory import AnyModelConfig from invokeai.backend.model_manager.load.model_cache.cache_record import CacheRecord +from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import ( + CachedModelWithPartialLoad, +) from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache from invokeai.backend.model_manager.taxonomy import AnyModel, SubModelType @@ -80,6 +83,13 @@ class LoadedModelWithoutConfig: """Return the model without locking it.""" return self._cache_record.cached_model.model + def repair_required_tensors_on_device(self) -> int: + """Repair required tensors that should be resident on the cached model's execution device.""" + cached_model = self._cache_record.cached_model + if not isinstance(cached_model, CachedModelWithPartialLoad): + return 0 + return cached_model.repair_required_tensors_on_compute_device() + class LoadedModel(LoadedModelWithoutConfig): """Context manager object that mediates transfer from RAM<->VRAM.""" diff --git a/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_with_partial_load.py b/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_with_partial_load.py index f80b017ba7..328978b45b 100644 --- a/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_with_partial_load.py +++ b/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_with_partial_load.py @@ -149,6 +149,27 @@ class CachedModelWithPartialLoad: """Unload all weights from VRAM.""" return self.partial_unload_from_vram(self.total_bytes()) + @torch.no_grad() + def repair_required_tensors_on_compute_device(self) -> int: + """Repair required non-autocast tensors that were left off the compute device. + + This can happen if an interrupted run leaves the model in a partially inconsistent state. Any repaired device + movement invalidates the cached VRAM accounting. + """ + cur_state_dict = self._model.state_dict() + keys_to_repair = { + key + for key in self._keys_in_modules_that_do_not_support_autocast + if cur_state_dict[key].device.type != self._compute_device.type + } + if len(keys_to_repair) == 0: + return 0 + + self._load_state_dict_with_device_conversion(cur_state_dict, keys_to_repair, self._compute_device) + self._move_non_persistent_buffers_to_device(self._compute_device) + self._cur_vram_bytes = None + return len(keys_to_repair) + def _load_state_dict_with_device_conversion( self, state_dict: dict[str, torch.Tensor], keys_to_convert: set[str], target_device: torch.device ): diff --git a/tests/app/invocations/test_cogview4_text_encoder.py b/tests/app/invocations/test_cogview4_text_encoder.py new file mode 100644 index 0000000000..81741d4138 --- /dev/null +++ b/tests/app/invocations/test_cogview4_text_encoder.py @@ -0,0 +1,80 @@ +from contextlib import contextmanager +from types import SimpleNamespace +from unittest.mock import MagicMock + +import torch + +from invokeai.app.invocations.cogview4_text_encoder import CogView4TextEncoderInvocation + + +class FakeGlmModel(torch.nn.Module): + def __init__(self): + super().__init__() + self.register_parameter("weight", torch.nn.Parameter(torch.ones(1))) + self.repaired = False + self.forward_input_device: torch.device | None = None + + def forward(self, input_ids: torch.Tensor, output_hidden_states: bool = False): + assert output_hidden_states + if not self.repaired: + raise RuntimeError("model must be repaired before forward") + + self.forward_input_device = input_ids.device + hidden = input_ids.unsqueeze(-1).float() + return SimpleNamespace(hidden_states=[hidden, hidden + 1]) + + +class FakeTokenizer: + pad_token_id = 0 + + def __call__(self, prompt, padding, max_length=None, truncation=None, add_special_tokens=None, return_tensors=None): + del prompt, padding, max_length, truncation, add_special_tokens, return_tensors + return SimpleNamespace(input_ids=torch.tensor([[1, 2, 3]], dtype=torch.long)) + + def batch_decode(self, input_ids): + del input_ids + return ["decoded"] + + +class FakeLoadedModel: + def __init__(self, model): + self._model = model + self.repair_calls = 0 + + @contextmanager + def model_on_device(self): + yield (None, self._model) + + def repair_required_tensors_on_device(self) -> int: + self.repair_calls += 1 + self._model.repaired = True + return 1 + + +def test_cogview4_text_encoder_repairs_model_before_forward(monkeypatch): + fake_model = FakeGlmModel() + fake_tokenizer = FakeTokenizer() + fake_model_info = FakeLoadedModel(fake_model) + fake_tokenizer_info = FakeLoadedModel(fake_tokenizer) + + mock_context = MagicMock() + mock_context.models.load.side_effect = [fake_model_info, fake_tokenizer_info] + mock_context.util.signal_progress = MagicMock() + mock_context.logger.warning = MagicMock() + + invocation = CogView4TextEncoderInvocation.model_construct( + prompt="test prompt", + glm_encoder=SimpleNamespace(text_encoder=SimpleNamespace(), tokenizer=SimpleNamespace()), + ) + + module_path = "invokeai.app.invocations.cogview4_text_encoder" + monkeypatch.setattr(f"{module_path}.GlmModel", FakeGlmModel) + monkeypatch.setattr(f"{module_path}.PreTrainedTokenizerFast", FakeTokenizer) + + embeds = invocation._glm_encode(mock_context, max_seq_len=16) + + assert fake_model_info.repair_calls == 1 + mock_context.logger.warning.assert_called_once() + mock_context.util.signal_progress.assert_called_once_with("Running GLM text encoder") + assert fake_model.forward_input_device == torch.device("cpu") + assert embeds.shape == (1, 16, 1) diff --git a/tests/backend/model_manager/load/model_cache/cached_model/test_repair_required_tensors.py b/tests/backend/model_manager/load/model_cache/cached_model/test_repair_required_tensors.py new file mode 100644 index 0000000000..306e655a18 --- /dev/null +++ b/tests/backend/model_manager/load/model_cache/cached_model/test_repair_required_tensors.py @@ -0,0 +1,47 @@ +import pytest +import torch + +from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import ( + CachedModelWithPartialLoad, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import ( + apply_custom_layers_to_model, +) + + +class ModelWithRequiredScale(torch.nn.Module): + def __init__(self): + super().__init__() + self.linear = torch.nn.Linear(4, 4) + self.scale = torch.nn.Parameter(torch.ones(4)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.linear(x) * self.scale + + +@pytest.mark.parametrize( + "device", + [ + pytest.param( + torch.device("cuda"), marks=pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA device") + ), + pytest.param( + torch.device("mps"), + marks=pytest.mark.skipif(not torch.backends.mps.is_available(), reason="requires MPS device"), + ), + ], +) +@pytest.mark.parametrize("keep_ram_copy", [True, False]) +@torch.no_grad() +def test_repair_required_tensors_on_compute_device(device: torch.device, keep_ram_copy: bool): + model = ModelWithRequiredScale() + apply_custom_layers_to_model(model, device_autocasting_enabled=True) + cached_model = CachedModelWithPartialLoad(model=model, compute_device=device, keep_ram_copy=keep_ram_copy) + + cached_model._cur_vram_bytes = 0 + repaired_tensors = cached_model.repair_required_tensors_on_compute_device() + + assert repaired_tensors == 1 + assert cached_model._cur_vram_bytes is None + assert model.scale.device.type == device.type + assert all(param.device.type == "cpu" for param in model.linear.parameters()) From b120ef51838a1ac33d6bba3192aa0756347709b9 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sun, 15 Mar 2026 16:01:09 +0100 Subject: [PATCH 02/56] ui: translations update from weblate (#8956) * translationBot(ui): update translation (Italian) Currently translated at 98.0% (2205 of 2250 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI * translationBot(ui): update translation files Updated by "Remove blank strings" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ Translation: InvokeAI/Web UI * translationBot(ui): update translation (Italian) Currently translated at 97.8% (2210 of 2259 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.8% (2224 of 2272 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 98.1% (2252 of 2295 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 98.0% (2264 of 2309 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Russian) Currently translated at 60.7% (1419 of 2334 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/ * translationBot(ui): update translation (Italian) Currently translated at 98.1% (2290 of 2334 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.7% (2319 of 2372 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.7% (2327 of 2380 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.7% (2328 of 2382 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.5% (2370 of 2429 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Finnish) Currently translated at 1.5% (37 of 2429 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fi/ * translationBot(ui): update translation (Italian) Currently translated at 97.5% (2373 of 2433 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ --------- Co-authored-by: Riccardo Giovanetti Co-authored-by: DustyShoe Co-authored-by: Ilmari Laakkonen --- invokeai/frontend/web/public/locales/fi.json | 26 +++++++++++++++++++- invokeai/frontend/web/public/locales/it.json | 5 ++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/public/locales/fi.json b/invokeai/frontend/web/public/locales/fi.json index f03c6f1aa1..54e5a66660 100644 --- a/invokeai/frontend/web/public/locales/fi.json +++ b/invokeai/frontend/web/public/locales/fi.json @@ -4,7 +4,8 @@ "uploadImage": "Lataa kuva", "invokeProgressBar": "Invoken edistymispalkki", "nextImage": "Seuraava kuva", - "previousImage": "Edellinen kuva" + "previousImage": "Edellinen kuva", + "uploadImages": "Lähetä Kuva(t)" }, "common": { "languagePickerLabel": "Kielen valinta", @@ -29,5 +30,28 @@ "galleryImageSize": "Kuvan koko", "gallerySettings": "Gallerian asetukset", "autoSwitchNewImages": "Vaihda uusiin kuviin automaattisesti" + }, + "modelManager": { + "t5Encoder": "T5-kooderi", + "qwen3Encoder": "Qwen3-kooderi", + "zImageVae": "VAE (valinnainen)", + "zImageQwen3Encoder": "Qwen3-kooderi (valinnainen)", + "zImageQwen3SourcePlaceholder": "Pakollinen, jos VAE/Enkooderi on tyhjä", + "flux2KleinVae": "VAE (valinnainen)", + "flux2KleinQwen3Encoder": "Qwen3-kooderi (valinnainen)" + }, + "auth": { + "login": { + "title": "Kirjaudu sisään InvokeAI:hin", + "password": "Salasana", + "passwordPlaceholder": "Salasana", + "signIn": "Kirjaudu sisään", + "signingIn": "Kirjaudutaan sisään...", + "loginFailed": "Kirjautuminen epäonnistui. Tarkista käyttäjätunnuksesi tiedot." + }, + "setup": { + "title": "Tervetuloa InvokeAI:hin", + "subtitle": "Määritä ensimmäiseksi järjestelmänvalvojan tili" + } } } diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index d17d36d5c0..7a6dafe4c7 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -3139,6 +3139,11 @@ "back": "Indietro", "cannotDeleteSelf": "Non puoi eliminare il tuo account", "cannotDeactivateSelf": "Non puoi disattivare il tuo account" + }, + "passwordStrength": { + "weak": "Password debole", + "moderate": "Password moderata", + "strong": "Password forte" } } } From 17da6bb9c3d219f708fc828b263824b74d08416b Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sun, 15 Mar 2026 11:14:35 -0400 Subject: [PATCH 03/56] Fix(UI): Replace boolean submenu icon with PiIntersectSquareBold (#8962) * change submenu icon to phosphor * Use PiIntersectSquareBold --- .../RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx index c321317a34..19b0278353 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx @@ -9,7 +9,8 @@ import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLaye import type { CanvasEntityIdentifier, CompositeOperation } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { CgPathBack, CgPathCrop, CgPathExclude, CgPathFront, CgPathIntersect } from 'react-icons/cg'; +import { CgPathBack, CgPathExclude, CgPathFront, CgPathIntersect } from 'react-icons/cg'; +import { PiIntersectSquareBold } from 'react-icons/pi'; export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { const { t } = useTranslation(); @@ -48,7 +49,7 @@ export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { const disabled = isBusy || !entityIdentifierBelowThisOne; return ( - }> + }> From 8375f95ea9934755b325cc54554a5593c331b6a8 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Thu, 19 Mar 2026 04:36:09 +0100 Subject: [PATCH 04/56] feat: add resolution presets and imageConfig support for Gemini 3 models Add combined resolution preset selector for external models that maps aspect ratio + image size to fixed dimensions. Gemini 3 Pro and 3.1 Flash now send imageConfig (aspectRatio + imageSize) via generationConfig instead of text-based aspect ratio hints used by Gemini 2.5 Flash. Backend: ExternalResolutionPreset model, resolution_presets capability field, image_size on ExternalGenerationRequest, and Gemini provider imageConfig logic. Frontend: ExternalSettingsAccordion with combo resolution select, dimension slider disabling for fixed-size models, and panel schema constraint wiring for Steps/Guidance/Seed controls. --- .../invocations/external_image_generation.py | 2 + .../external_generation_common.py | 1 + .../external_generation_default.py | 2 + .../external_generation/providers/gemini.py | 11 ++- .../model_manager/configs/external_api.py | 11 +++ .../backend/model_manager/starter_models.py | 44 ++++++++- invokeai/frontend/web/public/locales/en.json | 1 + .../controlLayers/store/paramsSlice.ts | 83 +++++++++++++--- .../src/features/controlLayers/store/types.ts | 2 + .../graph/generation/buildExternalGraph.ts | 1 + .../components/Core/ParamGuidance.tsx | 34 ++++--- .../parameters/components/Core/ParamSteps.tsx | 34 ++++--- .../DimensionsAspectRatioSelect.tsx | 7 +- .../Dimensions/DimensionsHeight.tsx | 5 +- .../DimensionsLockAspectRatioButton.tsx | 8 +- .../DimensionsSetOptimalSizeButton.tsx | 9 +- .../Dimensions/DimensionsSwapButton.tsx | 6 +- .../components/Dimensions/DimensionsWidth.tsx | 5 +- .../ExternalModelImageSizeSelect.tsx | 94 +++++++++++++++++++ .../ExternalModelResolutionSelect.tsx | 68 ++++++++++++++ .../components/Seed/ParamSeedNumberInput.tsx | 15 ++- .../parameters/util/externalPanelSchema.ts | 2 +- .../ExternalSettingsAccordion.tsx | 44 +++++++++ .../ParametersPanelCanvas.tsx | 2 + .../ParametersPanelGenerate.tsx | 2 + .../frontend/web/src/services/api/types.ts | 10 ++ .../test_external_generation_service.py | 1 + .../test_external_provider_adapters.py | 1 + 28 files changed, 455 insertions(+), 50 deletions(-) create mode 100644 invokeai/frontend/web/src/features/parameters/components/Dimensions/ExternalModelImageSizeSelect.tsx create mode 100644 invokeai/frontend/web/src/features/parameters/components/Dimensions/ExternalModelResolutionSelect.tsx create mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx diff --git a/invokeai/app/invocations/external_image_generation.py b/invokeai/app/invocations/external_image_generation.py index 983dc5caf5..c66b024bfa 100644 --- a/invokeai/app/invocations/external_image_generation.py +++ b/invokeai/app/invocations/external_image_generation.py @@ -39,6 +39,7 @@ class BaseExternalImageGenerationInvocation(BaseInvocation, WithMetadata, WithBo num_images: int = InputField(default=1, gt=0, description="Number of images to generate") width: int = InputField(default=1024, gt=0, description=FieldDescriptions.width) height: int = InputField(default=1024, gt=0, description=FieldDescriptions.height) + image_size: str | None = InputField(default=None, description="Image size preset (e.g. 1K, 2K, 4K)") steps: int | None = InputField(default=None, gt=0, description=FieldDescriptions.steps) guidance: float | None = InputField(default=None, ge=0, description="Guidance strength") init_image: ImageField | None = InputField(default=None, description="Init image for img2img/inpaint") @@ -91,6 +92,7 @@ class BaseExternalImageGenerationInvocation(BaseInvocation, WithMetadata, WithBo num_images=self.num_images, width=self.width, height=self.height, + image_size=self.image_size, steps=self.steps, guidance=self.guidance, init_image=init_image, diff --git a/invokeai/app/services/external_generation/external_generation_common.py b/invokeai/app/services/external_generation/external_generation_common.py index c1e2f4706f..a6746913c1 100644 --- a/invokeai/app/services/external_generation/external_generation_common.py +++ b/invokeai/app/services/external_generation/external_generation_common.py @@ -25,6 +25,7 @@ class ExternalGenerationRequest: num_images: int width: int height: int + image_size: str | None steps: int | None guidance: float | None init_image: PILImageType | None diff --git a/invokeai/app/services/external_generation/external_generation_default.py b/invokeai/app/services/external_generation/external_generation_default.py index ff54d71476..9265e63b7f 100644 --- a/invokeai/app/services/external_generation/external_generation_default.py +++ b/invokeai/app/services/external_generation/external_generation_default.py @@ -164,6 +164,7 @@ class ExternalGenerationService(ExternalGenerationServiceBase): num_images=request.num_images, width=request.width, height=request.height, + image_size=request.image_size, steps=request.steps, guidance=request.guidance, init_image=request.init_image, @@ -234,6 +235,7 @@ class ExternalGenerationService(ExternalGenerationServiceBase): num_images=request.num_images, width=width, height=height, + image_size=request.image_size, steps=request.steps, guidance=request.guidance, init_image=_resize_image(request.init_image, width, height, "RGB"), diff --git a/invokeai/app/services/external_generation/providers/gemini.py b/invokeai/app/services/external_generation/providers/gemini.py index 4d43431a14..855d64d945 100644 --- a/invokeai/app/services/external_generation/providers/gemini.py +++ b/invokeai/app/services/external_generation/providers/gemini.py @@ -73,6 +73,15 @@ class GeminiProvider(ExternalProvider): request.height, request.model.capabilities.allowed_aspect_ratios, ) + uses_image_config = request.model.capabilities.resolution_presets is not None + if uses_image_config: + image_config: dict[str, str] = {} + if aspect_ratio is not None: + image_config["aspectRatio"] = aspect_ratio + if request.image_size is not None: + image_config["imageSize"] = request.image_size + if image_config: + generation_config["imageConfig"] = image_config system_instruction = self._SYSTEM_INSTRUCTION if request.init_image is not None: system_instruction = ( @@ -80,7 +89,7 @@ class GeminiProvider(ExternalProvider): "Treat the prompt as an edit instruction and modify the image accordingly. " "Do not return the original image unchanged." ) - if aspect_ratio is not None: + if not uses_image_config and aspect_ratio is not None: system_instruction = f"{system_instruction} Use an aspect ratio of {aspect_ratio}." payload: dict[str, object] = { diff --git a/invokeai/backend/model_manager/configs/external_api.py b/invokeai/backend/model_manager/configs/external_api.py index 50c51e28cf..4d105a65f6 100644 --- a/invokeai/backend/model_manager/configs/external_api.py +++ b/invokeai/backend/model_manager/configs/external_api.py @@ -19,6 +19,16 @@ class ExternalImageSize(BaseModel): model_config = ConfigDict(extra="forbid") +class ExternalResolutionPreset(BaseModel): + label: str = Field(min_length=1, description="Display label, e.g. '1:1 (1K)'") + aspect_ratio: str = Field(min_length=1, description="Aspect ratio string, e.g. '1:1'") + image_size: str = Field(min_length=1, description="Image size preset, e.g. '1K'") + width: int = Field(gt=0) + height: int = Field(gt=0) + + model_config = ConfigDict(extra="forbid") + + class ExternalModelCapabilities(BaseModel): modes: list[ExternalGenerationMode] = Field(default_factory=lambda: ["txt2img"]) supports_reference_images: bool = Field(default=False) @@ -30,6 +40,7 @@ class ExternalModelCapabilities(BaseModel): max_image_size: ExternalImageSize | None = Field(default=None) allowed_aspect_ratios: list[str] | None = Field(default=None) aspect_ratio_sizes: dict[str, ExternalImageSize] | None = Field(default=None) + resolution_presets: list[ExternalResolutionPreset] | None = Field(default=None) max_reference_images: int | None = Field(default=None, gt=0) mask_format: ExternalMaskFormat = Field(default="none") input_image_required_for: list[ExternalGenerationMode] | None = Field(default=None) diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py index 0d6671cf7e..5e9d422681 100644 --- a/invokeai/backend/model_manager/starter_models.py +++ b/invokeai/backend/model_manager/starter_models.py @@ -7,6 +7,7 @@ from invokeai.backend.model_manager.configs.external_api import ( ExternalImageSize, ExternalModelCapabilities, ExternalModelPanelSchema, + ExternalResolutionPreset, ) from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType @@ -890,6 +891,45 @@ GEMINI_3_IMAGE_ALLOWED_ASPECT_RATIOS = [ ] GEMINI_3_IMAGE_MAX_SIZE = ExternalImageSize(width=4096, height=4096) + +def _gemini_3_resolution_presets( + image_sizes: list[str], + aspect_ratios: list[str] | None = None, +) -> list[ExternalResolutionPreset]: + """Build resolution presets for Gemini 3 models. + + Each preset combines an aspect ratio with an image size preset (512/1K/2K/4K). + Pixel dimensions are approximations based on the preset name (longest side). + """ + if aspect_ratios is None: + aspect_ratios = GEMINI_3_IMAGE_ALLOWED_ASPECT_RATIOS + base_pixels = {"512": 512, "1K": 1024, "2K": 2048, "4K": 4096} + presets: list[ExternalResolutionPreset] = [] + for image_size in image_sizes: + base = base_pixels[image_size] + for ratio_str in aspect_ratios: + w_part, h_part = (int(x) for x in ratio_str.split(":")) + if w_part >= h_part: + w = base + h = max(1, round(base * h_part / w_part)) + else: + h = base + w = max(1, round(base * w_part / h_part)) + presets.append( + ExternalResolutionPreset( + label=f"{ratio_str} ({image_size}) — {w}\u00d7{h}", + aspect_ratio=ratio_str, + image_size=image_size, + width=w, + height=h, + ) + ) + return presets + + +GEMINI_3_PRO_RESOLUTION_PRESETS = _gemini_3_resolution_presets(["1K", "2K", "4K"]) +GEMINI_3_1_FLASH_RESOLUTION_PRESETS = _gemini_3_resolution_presets(["512", "1K", "2K", "4K"]) + gemini_flash_image = StarterModel( name="Gemini 2.5 Flash Image", base=BaseModelType.External, @@ -936,7 +976,7 @@ gemini_pro_image_preview = StarterModel( name="Gemini 3 Pro Image Preview", base=BaseModelType.External, source="external://gemini/gemini-3-pro-image-preview", - description="Google Gemini 3 Pro image generation preview model (external API). Supports up to 14 reference images, including up to 6 object references and up to 5 character references. Supports 512/1K/2K/4K resolution presets. Requires a configured Gemini API key and may incur provider usage costs.", + description="Google Gemini 3 Pro image generation preview model (external API). Supports up to 14 reference images, including up to 6 object references and up to 5 character references. Supports 1K/2K/4K resolution presets. Requires a configured Gemini API key and may incur provider usage costs.", type=ModelType.ExternalImageGenerator, format=ModelFormat.ExternalApi, capabilities=ExternalModelCapabilities( @@ -949,6 +989,7 @@ gemini_pro_image_preview = StarterModel( max_images_per_request=1, max_image_size=GEMINI_3_IMAGE_MAX_SIZE, allowed_aspect_ratios=GEMINI_3_IMAGE_ALLOWED_ASPECT_RATIOS, + resolution_presets=GEMINI_3_PRO_RESOLUTION_PRESETS, ), default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), panel_schema=ExternalModelPanelSchema(prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}]), @@ -970,6 +1011,7 @@ gemini_3_1_flash_image_preview = StarterModel( max_images_per_request=1, max_image_size=GEMINI_3_IMAGE_MAX_SIZE, allowed_aspect_ratios=GEMINI_3_IMAGE_ALLOWED_ASPECT_RATIOS, + resolution_presets=GEMINI_3_1_FLASH_RESOLUTION_PRESETS, ), default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), panel_schema=ExternalModelPanelSchema(prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}]), diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c961b8e8fc..db515785f3 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1601,6 +1601,7 @@ "boxBlur": "Box Blur", "staged": "Staged", "resolution": "Resolution", + "imageSize": "Image Size", "modelDisabledForTrial": "Generating with {{modelName}} is not available on trial accounts. Visit your account settings to upgrade." }, "dynamicPrompts": { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index 29fa709907..87b3789a32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -42,7 +42,7 @@ import type { ParameterT5EncoderModel, ParameterVAEModel, } from 'features/parameters/types/parameterSchemas'; -import { hasExternalPanelControl } from 'features/parameters/util/externalPanelSchema'; +import { getExternalPanelControl, hasExternalPanelControl } from 'features/parameters/util/externalPanelSchema'; import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models'; import type { AnyModelConfigWithExternal } from 'services/api/types'; @@ -366,21 +366,30 @@ const slice = createSlice({ aspectRatioLockToggled: (state) => { state.dimensions.aspectRatio.isLocked = !state.dimensions.aspectRatio.isLocked; }, - aspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { - const { id } = action.payload; + aspectRatioIdChanged: ( + state, + action: PayloadAction<{ id: AspectRatioID; fixedSize?: { width: number; height: number } }> + ) => { + const { id, fixedSize } = action.payload; state.dimensions.aspectRatio.id = id; if (id === 'Free') { state.dimensions.aspectRatio.isLocked = false; } else { state.dimensions.aspectRatio.isLocked = true; - state.dimensions.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; - const { width, height } = calculateNewSize( - state.dimensions.aspectRatio.value, - state.dimensions.width * state.dimensions.height, - state.model?.base as BaseModelType | undefined - ); - state.dimensions.width = width; - state.dimensions.height = height; + if (fixedSize) { + state.dimensions.aspectRatio.value = fixedSize.width / fixedSize.height; + state.dimensions.width = fixedSize.width; + state.dimensions.height = fixedSize.height; + } else { + state.dimensions.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; + const { width, height } = calculateNewSize( + state.dimensions.aspectRatio.value, + state.dimensions.width * state.dimensions.height, + state.model?.base as BaseModelType | undefined + ); + state.dimensions.width = width; + state.dimensions.height = height; + } } }, dimensionsSwapped: (state) => { @@ -436,6 +445,21 @@ const slice = createSlice({ state.dimensions.height = bboxDims.height; } }, + imageSizeChanged: (state, action: PayloadAction) => { + state.imageSize = action.payload; + }, + resolutionPresetSelected: ( + state, + action: PayloadAction<{ imageSize: string; aspectRatio: string; width: number; height: number }> + ) => { + const { imageSize, aspectRatio, width, height } = action.payload; + state.imageSize = imageSize; + state.dimensions.width = width; + state.dimensions.height = height; + state.dimensions.aspectRatio.id = aspectRatio as AspectRatioID; + state.dimensions.aspectRatio.value = width / height; + state.dimensions.aspectRatio.isLocked = true; + }, paramsReset: (state) => resetState(state), }, extraReducers(builder) { @@ -567,6 +591,7 @@ export const { sizeOptimized, syncedToOptimalDimension, + resolutionPresetSelected, paramsReset, } = slice.actions; @@ -737,6 +762,24 @@ export const selectModelSupportsDimensions = createSelector(selectModel, selectM } return true; }); +export const selectStepsControl = createSelector(selectModelConfig, (modelConfig) => { + if (modelConfig && isExternalApiModelConfig(modelConfig)) { + return getExternalPanelControl(modelConfig, 'generation', 'steps'); + } + return null; +}); +export const selectGuidanceControl = createSelector(selectModelConfig, (modelConfig) => { + if (modelConfig && isExternalApiModelConfig(modelConfig)) { + return getExternalPanelControl(modelConfig, 'generation', 'guidance'); + } + return null; +}); +export const selectSeedControl = createSelector(selectModelConfig, (modelConfig) => { + if (modelConfig && isExternalApiModelConfig(modelConfig)) { + return getExternalPanelControl(modelConfig, 'image', 'seed'); + } + return null; +}); export const selectScheduler = createParamsSelector((params) => params.scheduler); export const selectFluxScheduler = createParamsSelector((params) => params.fluxScheduler); export const selectFluxDypePreset = createParamsSelector((params) => params.fluxDypePreset); @@ -786,6 +829,24 @@ export const selectAllowedAspectRatioIDs = createSelector(selectModelConfig, (mo const allowed = modelConfig.capabilities.allowed_aspect_ratios; return allowed?.length ? allowed : null; }); +export const selectAspectRatioSizes = createSelector(selectModelConfig, (modelConfig) => { + if (!modelConfig || !isExternalApiModelConfig(modelConfig)) { + return null; + } + return modelConfig.capabilities.aspect_ratio_sizes ?? null; +}); +export const selectResolutionPresets = createSelector(selectModelConfig, (modelConfig) => { + if (!modelConfig || !isExternalApiModelConfig(modelConfig)) { + return null; + } + return modelConfig.capabilities.resolution_presets ?? null; +}); +export const selectHasFixedDimensionSizes = createSelector( + selectAspectRatioSizes, + selectResolutionPresets, + (sizes, presets) => sizes !== null || (presets !== null && presets.length > 0) +); +export const selectImageSize = createParamsSelector((params) => params.imageSize); export const selectMainModelConfig = createSelector(selectModelConfig, (modelConfig) => { if (!modelConfig) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 40babc7bc8..8c62f73b8e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -757,6 +757,7 @@ export const zParamsState = z.object({ zImageSeedVarianceEnabled: z.boolean(), zImageSeedVarianceStrength: z.number().min(0).max(2), zImageSeedVarianceRandomizePercent: z.number().min(1).max(100), + imageSize: z.string().nullable().default(null), dimensions: zDimensionsState, }); export type ParamsState = z.infer; @@ -820,6 +821,7 @@ export const getInitialParamsState = (): ParamsState => ({ zImageSeedVarianceEnabled: false, zImageSeedVarianceStrength: 0.1, zImageSeedVarianceRandomizePercent: 50, + imageSize: null, dimensions: { width: 512, height: 512, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts index db3325d8fa..4f686a5986 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts @@ -74,6 +74,7 @@ export const buildExternalGraph = async (arg: GraphBuilderArg): Promise { const guidance = useAppSelector(selectGuidance); + const externalControl = useAppSelector(selectGuidanceControl); const dispatch = useAppDispatch(); const { t } = useTranslation(); const onChange = useCallback((v: number) => dispatch(setGuidance(v)), [dispatch]); + const sliderMin = externalControl?.slider_min ?? CONSTRAINTS.sliderMin; + const sliderMax = externalControl?.slider_max ?? CONSTRAINTS.sliderMax; + const numberInputMin = externalControl?.number_input_min ?? CONSTRAINTS.numberInputMin; + const numberInputMax = externalControl?.number_input_max ?? CONSTRAINTS.numberInputMax; + const fineStep = externalControl?.fine_step ?? CONSTRAINTS.fineStep; + const coarseStep = externalControl?.coarse_step ?? CONSTRAINTS.coarseStep; + const marks = useMemo( + () => externalControl?.marks ?? [sliderMin, Math.floor(sliderMax - (sliderMax - sliderMin) / 2), sliderMax], + [externalControl?.marks, sliderMin, sliderMax] + ); + return ( @@ -35,20 +47,20 @@ const ParamGuidance = () => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx index 31efe5d0a6..b6d810dd2e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx @@ -1,8 +1,8 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectSteps, setSteps } from 'features/controlLayers/store/paramsSlice'; -import { memo, useCallback } from 'react'; +import { selectSteps, selectStepsControl, setSteps } from 'features/controlLayers/store/paramsSlice'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const CONSTRAINTS = { @@ -19,6 +19,7 @@ export const MARKS = [CONSTRAINTS.sliderMin, Math.floor(CONSTRAINTS.sliderMax / const ParamSteps = () => { const steps = useAppSelector(selectSteps); + const externalControl = useAppSelector(selectStepsControl); const dispatch = useAppDispatch(); const { t } = useTranslation(); const onChange = useCallback( @@ -28,6 +29,17 @@ const ParamSteps = () => { [dispatch] ); + const sliderMin = externalControl?.slider_min ?? CONSTRAINTS.sliderMin; + const sliderMax = externalControl?.slider_max ?? CONSTRAINTS.sliderMax; + const numberInputMin = externalControl?.number_input_min ?? CONSTRAINTS.numberInputMin; + const numberInputMax = externalControl?.number_input_max ?? CONSTRAINTS.numberInputMax; + const fineStep = externalControl?.fine_step ?? CONSTRAINTS.fineStep; + const coarseStep = externalControl?.coarse_step ?? CONSTRAINTS.coarseStep; + const marks = useMemo( + () => externalControl?.marks ?? [sliderMin, Math.floor(sliderMax / 2), sliderMax], + [externalControl?.marks, sliderMin, sliderMax] + ); + return ( @@ -36,20 +48,20 @@ const ParamSteps = () => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx index 5e2952552c..5beb1231b0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx @@ -5,6 +5,7 @@ import { aspectRatioIdChanged, selectAllowedAspectRatioIDs, selectAspectRatioID, + selectAspectRatioSizes, } from 'features/controlLayers/store/paramsSlice'; import { isAspectRatioID, zAspectRatioID } from 'features/controlLayers/store/types'; import type { ChangeEventHandler } from 'react'; @@ -17,6 +18,7 @@ export const DimensionsAspectRatioSelect = memo(() => { const dispatch = useAppDispatch(); const id = useAppSelector(selectAspectRatioID); const allowedAspectRatios = useAppSelector(selectAllowedAspectRatioIDs); + const aspectRatioSizes = useAppSelector(selectAspectRatioSizes); const options = allowedAspectRatios ?? zAspectRatioID.options; const onChange = useCallback>( @@ -24,9 +26,10 @@ export const DimensionsAspectRatioSelect = memo(() => { if (!isAspectRatioID(e.target.value)) { return; } - dispatch(aspectRatioIdChanged({ id: e.target.value })); + const fixedSize = aspectRatioSizes?.[e.target.value] ?? undefined; + dispatch(aspectRatioIdChanged({ id: e.target.value, fixedSize })); }, - [dispatch] + [dispatch, aspectRatioSizes] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsHeight.tsx index 924187c1ed..81e8101462 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsHeight.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { heightChanged, selectHeight } from 'features/controlLayers/store/paramsSlice'; +import { heightChanged, selectHasFixedDimensionSizes, selectHeight } from 'features/controlLayers/store/paramsSlice'; import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -22,6 +22,7 @@ export const DimensionsHeight = memo(() => { const optimalDimension = useAppSelector(selectOptimalDimension); const height = useAppSelector(selectHeight); const gridSize = useAppSelector(selectGridSize); + const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes); const onChange = useCallback( (v: number) => { @@ -33,7 +34,7 @@ export const DimensionsHeight = memo(() => { const marks = useMemo(() => [CONSTRAINTS.sliderMin, optimalDimension, CONSTRAINTS.sliderMax], [optimalDimension]); return ( - + {t('parameters.height')} diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx index 6ab17147a7..e3e2c6d59e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx @@ -1,6 +1,10 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { aspectRatioLockToggled, selectAspectRatioIsLocked } from 'features/controlLayers/store/paramsSlice'; +import { + aspectRatioLockToggled, + selectAspectRatioIsLocked, + selectHasFixedDimensionSizes, +} from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi'; @@ -9,6 +13,7 @@ export const DimensionsLockAspectRatioButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isLocked = useAppSelector(selectAspectRatioIsLocked); + const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes); const onClick = useCallback(() => { dispatch(aspectRatioLockToggled()); @@ -22,6 +27,7 @@ export const DimensionsLockAspectRatioButton = memo(() => { variant={isLocked ? 'outline' : 'ghost'} size="sm" icon={isLocked ? : } + isDisabled={hasFixedSizes} /> ); }); diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSetOptimalSizeButton.tsx index c1c43f0cec..90f6262c5a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSetOptimalSizeButton.tsx @@ -1,6 +1,11 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectHeight, selectWidth, sizeOptimized } from 'features/controlLayers/store/paramsSlice'; +import { + selectHasFixedDimensionSizes, + selectHeight, + selectWidth, + sizeOptimized, +} from 'features/controlLayers/store/paramsSlice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension'; import { memo, useCallback, useMemo } from 'react'; @@ -13,6 +18,7 @@ export const DimensionsSetOptimalSizeButton = memo(() => { const width = useAppSelector(selectWidth); const height = useAppSelector(selectHeight); const optimalDimension = useAppSelector(selectOptimalDimension); + const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes); const isSizeTooSmall = useMemo( () => getIsSizeTooSmall(width, height, optimalDimension), [height, width, optimalDimension] @@ -43,6 +49,7 @@ export const DimensionsSetOptimalSizeButton = memo(() => { size="sm" icon={} colorScheme={isSizeTooSmall || isSizeTooLarge ? 'warning' : 'base'} + isDisabled={hasFixedSizes} /> ); }); diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx index 817a81996f..1b5923cddf 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx @@ -1,6 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { dimensionsSwapped } from 'features/controlLayers/store/paramsSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { dimensionsSwapped, selectHasFixedDimensionSizes } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsDownUpBold } from 'react-icons/pi'; @@ -8,6 +8,7 @@ import { PiArrowsDownUpBold } from 'react-icons/pi'; export const DimensionsSwapButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes); const onClick = useCallback(() => { dispatch(dimensionsSwapped()); }, [dispatch]); @@ -19,6 +20,7 @@ export const DimensionsSwapButton = memo(() => { variant="ghost" size="sm" icon={} + isDisabled={hasFixedSizes} /> ); }); diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsWidth.tsx index 20a754c5c3..c70a786c89 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsWidth.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectWidth, widthChanged } from 'features/controlLayers/store/paramsSlice'; +import { selectHasFixedDimensionSizes, selectWidth, widthChanged } from 'features/controlLayers/store/paramsSlice'; import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -22,6 +22,7 @@ export const DimensionsWidth = memo(() => { const width = useAppSelector(selectWidth); const optimalDimension = useAppSelector(selectOptimalDimension); const gridSize = useAppSelector(selectGridSize); + const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes); const onChange = useCallback( (v: number) => { @@ -33,7 +34,7 @@ export const DimensionsWidth = memo(() => { const marks = useMemo(() => [CONSTRAINTS.sliderMin, optimalDimension, CONSTRAINTS.sliderMax], [optimalDimension]); return ( - + {t('parameters.width')} diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/ExternalModelImageSizeSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/ExternalModelImageSizeSelect.tsx new file mode 100644 index 0000000000..0ad995cf72 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/ExternalModelImageSizeSelect.tsx @@ -0,0 +1,94 @@ +import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + resolutionPresetSelected, + selectAspectRatioID, + selectImageSize, + selectResolutionPresets, +} from 'features/controlLayers/store/paramsSlice'; +import type { ChangeEventHandler } from 'react'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretDownBold } from 'react-icons/pi'; + +const makeKey = (aspectRatio: string, imageSize: string) => `${aspectRatio}|${imageSize}`; + +export const ExternalModelImageSizeSelect = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const presets = useAppSelector(selectResolutionPresets); + const currentAspectRatio = useAppSelector(selectAspectRatioID); + const currentImageSize = useAppSelector(selectImageSize); + + const presetMap = useMemo(() => { + if (!presets) { + return null; + } + const map = new Map(); + for (const preset of presets) { + map.set(makeKey(preset.aspect_ratio, preset.image_size), preset); + } + return map; + }, [presets]); + + const selectedKey = useMemo(() => { + if (!presets || presets.length === 0) { + return ''; + } + if (currentImageSize && currentAspectRatio) { + const key = makeKey(currentAspectRatio, currentImageSize); + if (presetMap?.has(key)) { + return key; + } + } + // Fallback to first preset + return makeKey(presets[0]!.aspect_ratio, presets[0]!.image_size); + }, [presets, presetMap, currentAspectRatio, currentImageSize]); + + const onChange = useCallback>( + (e) => { + const preset = presetMap?.get(e.target.value); + if (!preset) { + return; + } + dispatch( + resolutionPresetSelected({ + imageSize: preset.image_size, + aspectRatio: preset.aspect_ratio, + width: preset.width, + height: preset.height, + }) + ); + }, + [dispatch, presetMap] + ); + + if (!presets || presets.length === 0) { + return null; + } + + return ( + + {t('parameters.resolution')} + + + ); +}); + +ExternalModelImageSizeSelect.displayName = 'ExternalModelImageSizeSelect'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/ExternalModelResolutionSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/ExternalModelResolutionSelect.tsx new file mode 100644 index 0000000000..babb4bcdec --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/ExternalModelResolutionSelect.tsx @@ -0,0 +1,68 @@ +import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + aspectRatioIdChanged, + selectAspectRatioID, + selectAspectRatioSizes, +} from 'features/controlLayers/store/paramsSlice'; +import { isAspectRatioID } from 'features/controlLayers/store/types'; +import type { ChangeEventHandler } from 'react'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretDownBold } from 'react-icons/pi'; + +export const ExternalModelResolutionSelect = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const aspectRatioID = useAppSelector(selectAspectRatioID); + const aspectRatioSizes = useAppSelector(selectAspectRatioSizes); + + const options = useMemo(() => { + if (!aspectRatioSizes) { + return []; + } + return Object.entries(aspectRatioSizes).map(([ratio, size]) => ({ + ratio, + label: `${ratio} (${size.width}×${size.height})`, + size, + })); + }, [aspectRatioSizes]); + + const onChange = useCallback>( + (e) => { + const ratio = e.target.value; + if (!isAspectRatioID(ratio)) { + return; + } + const fixedSize = aspectRatioSizes?.[ratio] ?? undefined; + dispatch(aspectRatioIdChanged({ id: ratio, fixedSize })); + }, + [dispatch, aspectRatioSizes] + ); + + if (!aspectRatioSizes) { + return null; + } + + return ( + + {t('parameters.resolution')} + + + ); +}); + +ExternalModelResolutionSelect.displayName = 'ExternalModelResolutionSelect'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx index d7e5ac2ecc..7c5ab19ec0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx @@ -2,13 +2,19 @@ import { CompositeNumberInput, FormControl, FormLabel } from '@invoke-ai/ui-libr import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectSeed, selectShouldRandomizeSeed, setSeed } from 'features/controlLayers/store/paramsSlice'; +import { + selectSeed, + selectSeedControl, + selectShouldRandomizeSeed, + setSeed, +} from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const ParamSeedNumberInput = memo(() => { const seed = useAppSelector(selectSeed); const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); + const externalControl = useAppSelector(selectSeedControl); const { t } = useTranslation(); @@ -22,9 +28,10 @@ export const ParamSeedNumberInput = memo(() => { {t('parameters.seed')} modelConfig.panel_schema ?? buildExternalPanelSchemaFromCapabilities(modelConfig.capabilities); -const getExternalPanelControl = ( +export const getExternalPanelControl = ( modelConfig: ExternalApiModelConfig, panel: ExternalPanelName, controlName: ExternalPanelControlName diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx new file mode 100644 index 0000000000..deb3ea2c83 --- /dev/null +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx @@ -0,0 +1,44 @@ +import type { FormLabelProps } from '@invoke-ai/ui-library'; +import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectIsExternal } from 'features/controlLayers/store/paramsSlice'; +import { ExternalModelImageSizeSelect } from 'features/parameters/components/Dimensions/ExternalModelImageSizeSelect'; +import { ExternalModelResolutionSelect } from 'features/parameters/components/Dimensions/ExternalModelResolutionSelect'; +import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const formLabelProps: FormLabelProps = { + minW: '4rem', +}; + +export const ExternalSettingsAccordion = memo(() => { + const { t } = useTranslation(); + const isExternal = useAppSelector(selectIsExternal); + const { isOpen, onToggle } = useStandaloneAccordionToggle({ + id: 'external-settings', + defaultIsOpen: true, + }); + + if (!isExternal) { + return null; + } + + return ( + + + + + + + + + ); +}); + +ExternalSettingsAccordion.displayName = 'ExternalSettingsAccordion'; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx index 38f99590ec..dec9a9c6cf 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx @@ -6,6 +6,7 @@ import { selectIsCogView4, selectIsExternal, selectIsSDXL } from 'features/contr import { Prompts } from 'features/parameters/components/Prompts/Prompts'; import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion'; import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion'; +import { ExternalSettingsAccordion } from 'features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion'; import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion'; import { CanvasTabImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/CanvasTabImageSettingsAccordion'; import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion'; @@ -47,6 +48,7 @@ export const ParametersPanelCanvas = memo(() => { {!isExternal && } {isSDXL && } {!isCogview4 && !isExternal && } + {isExternal && } diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx index 5d1ea8193b..06a122cc4e 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx @@ -5,6 +5,7 @@ import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/con import { selectIsCogView4, selectIsExternal, selectIsSDXL } from 'features/controlLayers/store/paramsSlice'; import { Prompts } from 'features/parameters/components/Prompts/Prompts'; import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion'; +import { ExternalSettingsAccordion } from 'features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion'; import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion'; import { GenerateTabImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/GenerateTabImageSettingsAccordion'; import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion'; @@ -45,6 +46,7 @@ export const ParametersPanelGenerate = memo(() => { {isSDXL && } {!isCogview4 && !isExternal && } + {isExternal && } diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 6a5b539d7e..6c89dbc76b 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -131,6 +131,14 @@ export type ExternalImageSize = { height: number; }; +type ExternalResolutionPreset = { + label: string; + aspect_ratio: string; + image_size: string; + width: number; + height: number; +}; + export type ExternalModelCapabilities = { modes: ('txt2img' | 'img2img' | 'inpaint')[]; supports_reference_images?: boolean; @@ -141,6 +149,8 @@ export type ExternalModelCapabilities = { max_images_per_request?: number | null; max_image_size?: ExternalImageSize | null; allowed_aspect_ratios?: string[] | null; + aspect_ratio_sizes?: Record | null; + resolution_presets?: ExternalResolutionPreset[] | null; max_reference_images?: number | null; mask_format?: 'alpha' | 'binary' | 'none'; input_image_required_for?: ('txt2img' | 'img2img' | 'inpaint')[] | null; diff --git a/tests/app/services/external_generation/test_external_generation_service.py b/tests/app/services/external_generation/test_external_generation_service.py index fe3dd9d532..4ad0899c9c 100644 --- a/tests/app/services/external_generation/test_external_generation_service.py +++ b/tests/app/services/external_generation/test_external_generation_service.py @@ -74,6 +74,7 @@ def _build_request( num_images=num_images, width=width, height=height, + image_size=None, steps=10, guidance=guidance, init_image=init_image, diff --git a/tests/app/services/external_generation/test_external_provider_adapters.py b/tests/app/services/external_generation/test_external_provider_adapters.py index c4da4c913b..a7493ec056 100644 --- a/tests/app/services/external_generation/test_external_provider_adapters.py +++ b/tests/app/services/external_generation/test_external_provider_adapters.py @@ -64,6 +64,7 @@ def _build_request( num_images=1, width=256, height=256, + image_size=None, steps=20, guidance=5.5, init_image=init_image, From d8d0ebc356599bc5eb67925e08e83bb3e38b6a0c Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Fri, 20 Mar 2026 08:17:16 +0100 Subject: [PATCH 05/56] Remove unused external model fields and add provider-specific parameters - Remove negative_prompt, steps, guidance, reference_image_weights, reference_image_modes from external model nodes (unused by any provider) - Remove supports_negative_prompt, supports_steps, supports_guidance from ExternalModelCapabilities - Add provider_options dict to ExternalGenerationRequest for provider-specific parameters - Add OpenAI-specific fields: quality, background, input_fidelity - Add Gemini-specific fields: temperature, thinking_level - Add new OpenAI starter models: GPT Image 1.5, GPT Image 1 Mini, DALL-E 3, DALL-E 2 - Fix OpenAI provider to use output_format (GPT Image) vs response_format (DALL-E) and send model ID in requests - Add fixed aspect ratio sizes for OpenAI models (bucketing) - Add ExternalProviderRateLimitError with retry logic for 429 responses - Add provider-specific UI components in ExternalSettingsAccordion - Simplify ParamSteps/ParamGuidance by removing dead external overrides - Update all backend and frontend tests --- .../invocations/external_image_generation.py | 61 ++++++---- .../services/external_generation/errors.py | 10 ++ .../external_generation_common.py | 6 +- .../external_generation_default.py | 42 +++++-- .../external_generation/providers/gemini.py | 23 +++- .../external_generation/providers/openai.py | 58 +++++++-- .../model_manager/configs/external_api.py | 4 +- .../backend/model_manager/starter_models.py | 115 +++++++++++++++--- .../controlLayers/store/paramsSlice.test.ts | 30 ++--- .../controlLayers/store/paramsSlice.ts | 78 +++++++----- .../src/features/controlLayers/store/types.ts | 12 ++ .../ExternalProvidersForm.tsx | 1 + .../subpanels/ModelPanel/ModelEdit.tsx | 8 -- .../generation/buildExternalGraph.test.ts | 41 +------ .../graph/generation/buildExternalGraph.ts | 33 +++-- .../Bbox/BboxAspectRatioSelect.test.tsx | 2 - .../components/Core/ParamGuidance.tsx | 34 ++---- .../parameters/components/Core/ParamSteps.tsx | 34 ++---- .../DimensionsAspectRatioSelect.test.tsx | 2 - .../External/GeminiProviderOptions.tsx | 74 +++++++++++ .../External/OpenAIProviderOptions.tsx | 84 +++++++++++++ .../MainModel/mainModelPickerUtils.test.ts | 1 - .../parameters/util/externalPanelSchema.ts | 10 +- .../ExternalSettingsAccordion.tsx | 7 +- .../test_external_image_generation.py | 26 ---- tests/app/routers/test_model_manager.py | 9 +- .../test_external_generation_service.py | 21 +--- .../test_external_provider_adapters.py | 7 +- 28 files changed, 530 insertions(+), 303 deletions(-) create mode 100644 invokeai/frontend/web/src/features/parameters/components/External/GeminiProviderOptions.tsx create mode 100644 invokeai/frontend/web/src/features/parameters/components/External/OpenAIProviderOptions.tsx diff --git a/invokeai/app/invocations/external_image_generation.py b/invokeai/app/invocations/external_image_generation.py index c66b024bfa..b66affe9b0 100644 --- a/invokeai/app/invocations/external_image_generation.py +++ b/invokeai/app/invocations/external_image_generation.py @@ -1,4 +1,4 @@ -from typing import Any, ClassVar +from typing import Any, ClassVar, Literal from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation from invokeai.app.invocations.fields import ( @@ -34,19 +34,18 @@ class BaseExternalImageGenerationInvocation(BaseInvocation, WithMetadata, WithBo ) mode: ExternalGenerationMode = InputField(default="txt2img", description="Generation mode") prompt: str = InputField(description="Prompt") - negative_prompt: str | None = InputField(default=None, description="Negative prompt") seed: int | None = InputField(default=None, description=FieldDescriptions.seed) num_images: int = InputField(default=1, gt=0, description="Number of images to generate") width: int = InputField(default=1024, gt=0, description=FieldDescriptions.width) height: int = InputField(default=1024, gt=0, description=FieldDescriptions.height) image_size: str | None = InputField(default=None, description="Image size preset (e.g. 1K, 2K, 4K)") - steps: int | None = InputField(default=None, gt=0, description=FieldDescriptions.steps) - guidance: float | None = InputField(default=None, ge=0, description="Guidance strength") init_image: ImageField | None = InputField(default=None, description="Init image for img2img/inpaint") mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint") reference_images: list[ImageField] = InputField(default=[], description="Reference images") - reference_image_weights: list[float] | None = InputField(default=None, description="Reference image weights") - reference_image_modes: list[str] | None = InputField(default=None, description="Reference image modes") + + def _build_provider_options(self) -> dict[str, Any] | None: + """Override in provider-specific subclasses to pass extra options.""" + return None def invoke(self, context: InvocationContext) -> ImageCollectionOutput: model_config = context.models.get_config(self.model) @@ -66,39 +65,25 @@ class BaseExternalImageGenerationInvocation(BaseInvocation, WithMetadata, WithBo if self.mask_image is not None: mask_image = context.images.get_pil(self.mask_image.image_name, mode="L") - if self.reference_image_weights is not None and len(self.reference_image_weights) != len(self.reference_images): - raise ValueError("reference_image_weights must match reference_images length") - - if self.reference_image_modes is not None and len(self.reference_image_modes) != len(self.reference_images): - raise ValueError("reference_image_modes must match reference_images length") - reference_images: list[ExternalReferenceImage] = [] - for index, image_field in enumerate(self.reference_images): + for image_field in self.reference_images: reference_image = context.images.get_pil(image_field.image_name, mode="RGB") - weight = None - mode = None - if self.reference_image_weights is not None: - weight = self.reference_image_weights[index] - if self.reference_image_modes is not None: - mode = self.reference_image_modes[index] - reference_images.append(ExternalReferenceImage(image=reference_image, weight=weight, mode=mode)) + reference_images.append(ExternalReferenceImage(image=reference_image)) request = ExternalGenerationRequest( model=model_config, mode=self.mode, prompt=self.prompt, - negative_prompt=self.negative_prompt, seed=self.seed, num_images=self.num_images, width=self.width, height=self.height, image_size=self.image_size, - steps=self.steps, - guidance=self.guidance, init_image=init_image, mask_image=mask_image, reference_images=reference_images, metadata=self._build_request_metadata(), + provider_options=self._build_provider_options(), ) result = context._services.external_generation.generate(request) @@ -174,6 +159,23 @@ class OpenAIImageGenerationInvocation(BaseExternalImageGenerationInvocation): provider_id = "openai" + quality: Literal["auto", "high", "medium", "low"] = InputField(default="auto", description="Output image quality") + background: Literal["auto", "transparent", "opaque"] = InputField( + default="auto", description="Background transparency handling" + ) + input_fidelity: Literal["low", "high"] | None = InputField( + default=None, description="Fidelity to source images (edits only)" + ) + + def _build_provider_options(self) -> dict[str, Any]: + options: dict[str, Any] = { + "quality": self.quality, + "background": self.background, + } + if self.input_fidelity is not None: + options["input_fidelity"] = self.input_fidelity + return options + @invocation( "gemini_image_generation", @@ -186,3 +188,16 @@ class GeminiImageGenerationInvocation(BaseExternalImageGenerationInvocation): """Generate images using a Gemini-hosted external model.""" provider_id = "gemini" + + temperature: float | None = InputField(default=None, ge=0.0, le=2.0, description="Sampling temperature") + thinking_level: Literal["minimal", "high"] | None = InputField( + default=None, description="Thinking level for image generation" + ) + + def _build_provider_options(self) -> dict[str, Any] | None: + options: dict[str, Any] = {} + if self.temperature is not None: + options["temperature"] = self.temperature + if self.thinking_level is not None: + options["thinking_level"] = self.thinking_level + return options or None diff --git a/invokeai/app/services/external_generation/errors.py b/invokeai/app/services/external_generation/errors.py index 9980b39bc4..f61a6a8c73 100644 --- a/invokeai/app/services/external_generation/errors.py +++ b/invokeai/app/services/external_generation/errors.py @@ -16,3 +16,13 @@ class ExternalProviderCapabilityError(ExternalGenerationError): class ExternalProviderRequestError(ExternalGenerationError): """Raised when a provider rejects the request or returns an error.""" + + +class ExternalProviderRateLimitError(ExternalProviderRequestError): + """Raised when a provider returns HTTP 429 (rate limit exceeded).""" + + retry_after: float | None + + def __init__(self, message: str, retry_after: float | None = None) -> None: + super().__init__(message) + self.retry_after = retry_after diff --git a/invokeai/app/services/external_generation/external_generation_common.py b/invokeai/app/services/external_generation/external_generation_common.py index a6746913c1..f14bff52dd 100644 --- a/invokeai/app/services/external_generation/external_generation_common.py +++ b/invokeai/app/services/external_generation/external_generation_common.py @@ -11,8 +11,6 @@ from invokeai.backend.model_manager.configs.external_api import ExternalApiModel @dataclass(frozen=True) class ExternalReferenceImage: image: PILImageType - weight: float | None = None - mode: str | None = None @dataclass(frozen=True) @@ -20,18 +18,16 @@ class ExternalGenerationRequest: model: ExternalApiModelConfig mode: ExternalGenerationMode prompt: str - negative_prompt: str | None seed: int | None num_images: int width: int height: int image_size: str | None - steps: int | None - guidance: float | None init_image: PILImageType | None mask_image: PILImageType | None reference_images: list[ExternalReferenceImage] metadata: dict[str, Any] | None + provider_options: dict[str, Any] | None = None @dataclass(frozen=True) diff --git a/invokeai/app/services/external_generation/external_generation_default.py b/invokeai/app/services/external_generation/external_generation_default.py index 9265e63b7f..2622aa9b1c 100644 --- a/invokeai/app/services/external_generation/external_generation_default.py +++ b/invokeai/app/services/external_generation/external_generation_default.py @@ -1,5 +1,6 @@ from __future__ import annotations +import time from logging import Logger from typing import TYPE_CHECKING @@ -10,6 +11,7 @@ from invokeai.app.services.external_generation.errors import ( ExternalProviderCapabilityError, ExternalProviderNotConfiguredError, ExternalProviderNotFoundError, + ExternalProviderRateLimitError, ) from invokeai.app.services.external_generation.external_generation_base import ( ExternalGenerationServiceBase, @@ -52,7 +54,7 @@ class ExternalGenerationService(ExternalGenerationServiceBase): request = self._bucket_request(request) self._validate_request(request) - result = provider.generate(request) + result = self._generate_with_retry(provider, request) if resize_to_original_inpaint_size is None: return result @@ -60,6 +62,30 @@ class ExternalGenerationService(ExternalGenerationServiceBase): width, height = resize_to_original_inpaint_size return _resize_result_images(result, width, height) + _MAX_RETRIES = 3 + _DEFAULT_RETRY_DELAY = 10.0 + _MAX_RETRY_DELAY = 60.0 + + def _generate_with_retry( + self, provider: ExternalProvider, request: ExternalGenerationRequest + ) -> ExternalGenerationResult: + for attempt in range(self._MAX_RETRIES): + try: + return provider.generate(request) + except ExternalProviderRateLimitError as exc: + if attempt == self._MAX_RETRIES - 1: + raise + delay = min(exc.retry_after or self._DEFAULT_RETRY_DELAY, self._MAX_RETRY_DELAY) + self._logger.warning( + "Rate limited by %s (attempt %d/%d), retrying in %.0fs", + request.model.provider_id, + attempt + 1, + self._MAX_RETRIES, + delay, + ) + time.sleep(delay) + raise ExternalProviderRateLimitError("Rate limit exceeded after all retries") + def get_provider_statuses(self) -> dict[str, ExternalProviderStatus]: return {provider_id: provider.get_status() for provider_id, provider in self._providers.items()} @@ -77,15 +103,9 @@ class ExternalGenerationService(ExternalGenerationServiceBase): if request.mode not in capabilities.modes: raise ExternalProviderCapabilityError(f"Mode '{request.mode}' is not supported by {request.model.name}") - if request.negative_prompt and not capabilities.supports_negative_prompt: - raise ExternalProviderCapabilityError(f"Negative prompts are not supported by {request.model.name}") - if request.seed is not None and not capabilities.supports_seed: raise ExternalProviderCapabilityError(f"Seed control is not supported by {request.model.name}") - if request.guidance is not None and not capabilities.supports_guidance: - raise ExternalProviderCapabilityError(f"Guidance is not supported by {request.model.name}") - if request.reference_images and not capabilities.supports_reference_images: raise ExternalProviderCapabilityError(f"Reference images are not supported by {request.model.name}") @@ -159,18 +179,16 @@ class ExternalGenerationService(ExternalGenerationServiceBase): model=record, mode=request.mode, prompt=request.prompt, - negative_prompt=request.negative_prompt, seed=request.seed, num_images=request.num_images, width=request.width, height=request.height, image_size=request.image_size, - steps=request.steps, - guidance=request.guidance, init_image=request.init_image, mask_image=request.mask_image, reference_images=request.reference_images, metadata=request.metadata, + provider_options=request.provider_options, ) def _bucket_request(self, request: ExternalGenerationRequest) -> ExternalGenerationRequest: @@ -230,18 +248,16 @@ class ExternalGenerationService(ExternalGenerationServiceBase): model=request.model, mode=request.mode, prompt=request.prompt, - negative_prompt=request.negative_prompt, seed=request.seed, num_images=request.num_images, width=width, height=height, image_size=request.image_size, - steps=request.steps, - guidance=request.guidance, init_image=_resize_image(request.init_image, width, height, "RGB"), mask_image=_resize_image(request.mask_image, width, height, "L"), reference_images=request.reference_images, metadata=request.metadata, + provider_options=request.provider_options, ) diff --git a/invokeai/app/services/external_generation/providers/gemini.py b/invokeai/app/services/external_generation/providers/gemini.py index 855d64d945..7f289cced7 100644 --- a/invokeai/app/services/external_generation/providers/gemini.py +++ b/invokeai/app/services/external_generation/providers/gemini.py @@ -6,7 +6,7 @@ import uuid import requests from PIL.Image import Image as PILImageType -from invokeai.app.services.external_generation.errors import ExternalProviderRequestError +from invokeai.app.services.external_generation.errors import ExternalProviderRateLimitError, ExternalProviderRequestError from invokeai.app.services.external_generation.external_generation_base import ExternalProvider from invokeai.app.services.external_generation.external_generation_common import ( ExternalGeneratedImage, @@ -64,10 +64,14 @@ class GeminiProvider(ExternalProvider): } ) + opts = request.provider_options or {} + generation_config: dict[str, object] = { "candidateCount": request.num_images, "responseModalities": ["IMAGE"], } + if "temperature" in opts: + generation_config["temperature"] = opts["temperature"] aspect_ratio = _select_aspect_ratio( request.width, request.height, @@ -97,6 +101,8 @@ class GeminiProvider(ExternalProvider): "contents": [{"role": "user", "parts": request_parts}], "generationConfig": generation_config, } + if "thinking_level" in opts: + payload["thinkingConfig"] = {"thinkingLevel": opts["thinking_level"].upper()} self._dump_debug_payload("request", payload) @@ -108,6 +114,12 @@ class GeminiProvider(ExternalProvider): ) if not response.ok: + if response.status_code == 429: + retry_after = _parse_retry_after(response.headers.get("retry-after")) + raise ExternalProviderRateLimitError( + f"Gemini rate limit exceeded. {f'Retry after {retry_after:.0f}s.' if retry_after else 'Please try again later.'}", + retry_after=retry_after, + ) raise ExternalProviderRequestError( f"Gemini request failed with status {response.status_code} for model '{model_id}': {response.text}" ) @@ -252,6 +264,15 @@ def _parse_ratio(value: str) -> float | None: return numerator / denominator +def _parse_retry_after(value: str | None) -> float | None: + if not value: + return None + try: + return float(value) + except ValueError: + return None + + def _gcd(a: int, b: int) -> int: while b: a, b = b, a % b diff --git a/invokeai/app/services/external_generation/providers/openai.py b/invokeai/app/services/external_generation/providers/openai.py index f06491a225..5051cf9cf1 100644 --- a/invokeai/app/services/external_generation/providers/openai.py +++ b/invokeai/app/services/external_generation/providers/openai.py @@ -5,7 +5,7 @@ import io import requests from PIL.Image import Image as PILImageType -from invokeai.app.services.external_generation.errors import ExternalProviderRequestError +from invokeai.app.services.external_generation.errors import ExternalProviderRateLimitError, ExternalProviderRequestError from invokeai.app.services.external_generation.external_generation_base import ExternalProvider from invokeai.app.services.external_generation.external_generation_common import ( ExternalGeneratedImage, @@ -18,6 +18,8 @@ from invokeai.app.services.external_generation.image_utils import decode_image_b class OpenAIProvider(ExternalProvider): provider_id = "openai" + _GPT_IMAGE_MODELS = {"gpt-image-1", "gpt-image-1.5", "gpt-image-1-mini"} + def is_configured(self) -> bool: return bool(self._app_config.external_openai_api_key) @@ -26,21 +28,33 @@ class OpenAIProvider(ExternalProvider): if not api_key: raise ExternalProviderRequestError("OpenAI API key is not configured") + model_id = request.model.provider_model_id + is_gpt_image = model_id in self._GPT_IMAGE_MODELS size = f"{request.width}x{request.height}" base_url = (self._app_config.external_openai_base_url or "https://api.openai.com").rstrip("/") headers = {"Authorization": f"Bearer {api_key}"} use_edits_endpoint = request.mode != "txt2img" or bool(request.reference_images) + opts = request.provider_options or {} + if not use_edits_endpoint: payload: dict[str, object] = { + "model": model_id, "prompt": request.prompt, "n": request.num_images, "size": size, - "response_format": "b64_json", } - if request.seed is not None: - payload["seed"] = request.seed + # GPT Image models use output_format; DALL-E uses response_format + if is_gpt_image: + payload["output_format"] = "png" + else: + payload["response_format"] = "b64_json" + if is_gpt_image: + if opts.get("quality") and opts["quality"] != "auto": + payload["quality"] = opts["quality"] + if opts.get("background") and opts["background"] != "auto": + payload["background"] = opts["background"] response = requests.post( f"{base_url}/v1/images/generations", headers=headers, @@ -72,11 +86,22 @@ class OpenAIProvider(ExternalProvider): files.append(("mask", ("mask.png", mask_buffer, "image/png"))) data: dict[str, object] = { + "model": model_id, "prompt": request.prompt, "n": request.num_images, "size": size, - "response_format": "b64_json", } + if is_gpt_image: + data["output_format"] = "png" + else: + data["response_format"] = "b64_json" + if is_gpt_image: + if opts.get("quality") and opts["quality"] != "auto": + data["quality"] = opts["quality"] + if opts.get("background") and opts["background"] != "auto": + data["background"] = opts["background"] + if opts.get("input_fidelity"): + data["input_fidelity"] = opts["input_fidelity"] response = requests.post( f"{base_url}/v1/images/edits", headers=headers, @@ -86,15 +111,21 @@ class OpenAIProvider(ExternalProvider): ) if not response.ok: + if response.status_code == 429: + retry_after = _parse_retry_after(response.headers.get("retry-after")) + raise ExternalProviderRateLimitError( + f"OpenAI rate limit exceeded. {f'Retry after {retry_after:.0f}s.' if retry_after else 'Please try again later.'}", + retry_after=retry_after, + ) raise ExternalProviderRequestError( f"OpenAI request failed with status {response.status_code}: {response.text}" ) - payload = response.json() - if not isinstance(payload, dict): + response_payload = response.json() + if not isinstance(response_payload, dict): raise ExternalProviderRequestError("OpenAI response payload was not a JSON object") images: list[ExternalGeneratedImage] = [] - data_items = payload.get("data") + data_items = response_payload.get("data") if not isinstance(data_items, list): raise ExternalProviderRequestError("OpenAI response payload missing image data") for item in data_items: @@ -112,5 +143,14 @@ class OpenAIProvider(ExternalProvider): images=images, seed_used=request.seed, provider_request_id=response.headers.get("x-request-id"), - provider_metadata={"model": request.model.provider_model_id}, + provider_metadata={"model": model_id}, ) + + +def _parse_retry_after(value: str | None) -> float | None: + if not value: + return None + try: + return float(value) + except ValueError: + return None diff --git a/invokeai/backend/model_manager/configs/external_api.py b/invokeai/backend/model_manager/configs/external_api.py index 4d105a65f6..da58cba410 100644 --- a/invokeai/backend/model_manager/configs/external_api.py +++ b/invokeai/backend/model_manager/configs/external_api.py @@ -9,7 +9,7 @@ from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ExternalGenerationMode = Literal["txt2img", "img2img", "inpaint"] ExternalMaskFormat = Literal["alpha", "binary", "none"] -ExternalPanelControlName = Literal["negative_prompt", "reference_images", "dimensions", "seed", "steps", "guidance"] +ExternalPanelControlName = Literal["reference_images", "dimensions", "seed"] class ExternalImageSize(BaseModel): @@ -51,8 +51,6 @@ class ExternalModelCapabilities(BaseModel): class ExternalApiModelDefaultSettings(BaseModel): width: int | None = Field(default=None, gt=0) height: int | None = Field(default=None, gt=0) - steps: int | None = Field(default=None, gt=0) - guidance: float | None = Field(default=None, gt=0) num_images: int | None = Field(default=None, gt=0) model_config = ConfigDict(extra="forbid") diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py index 5e9d422681..f047dc48e8 100644 --- a/invokeai/backend/model_manager/starter_models.py +++ b/invokeai/backend/model_manager/starter_models.py @@ -939,9 +939,9 @@ gemini_flash_image = StarterModel( format=ModelFormat.ExternalApi, capabilities=ExternalModelCapabilities( modes=["txt2img", "img2img", "inpaint"], - supports_negative_prompt=True, + supports_seed=True, - supports_guidance=True, + supports_reference_images=True, max_images_per_request=1, allowed_aspect_ratios=[ @@ -981,9 +981,9 @@ gemini_pro_image_preview = StarterModel( format=ModelFormat.ExternalApi, capabilities=ExternalModelCapabilities( modes=["txt2img", "img2img", "inpaint"], - supports_negative_prompt=True, + supports_seed=True, - supports_guidance=True, + supports_reference_images=True, max_reference_images=14, max_images_per_request=1, @@ -1003,9 +1003,9 @@ gemini_3_1_flash_image_preview = StarterModel( format=ModelFormat.ExternalApi, capabilities=ExternalModelCapabilities( modes=["txt2img", "img2img", "inpaint"], - supports_negative_prompt=True, + supports_seed=True, - supports_guidance=True, + supports_reference_images=True, max_reference_images=14, max_images_per_request=1, @@ -1016,23 +1016,104 @@ gemini_3_1_flash_image_preview = StarterModel( default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), panel_schema=ExternalModelPanelSchema(prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}]), ) -openai_gpt_image_1 = StarterModel( - name="ChatGPT Image", +OPENAI_GPT_IMAGE_ASPECT_RATIOS = ["1:1", "3:2", "2:3"] +OPENAI_GPT_IMAGE_ASPECT_RATIO_SIZES = { + "1:1": ExternalImageSize(width=1024, height=1024), + "3:2": ExternalImageSize(width=1536, height=1024), + "2:3": ExternalImageSize(width=1024, height=1536), +} +OPENAI_GPT_IMAGE_PANEL_SCHEMA = ExternalModelPanelSchema( + prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}] +) + +openai_gpt_image_1_5 = StarterModel( + name="GPT Image 1.5", base=BaseModelType.External, - source="external://openai/gpt-image-1", - description="OpenAI GPT-Image-1 image generation model (external API). Requires a configured OpenAI API key and may incur provider usage costs.", + source="external://openai/gpt-image-1.5", + description="OpenAI GPT-Image-1.5 image generation model. Fastest and most affordable GPT image model. Requires a configured OpenAI API key and may incur provider usage costs.", type=ModelType.ExternalImageGenerator, format=ModelFormat.ExternalApi, capabilities=ExternalModelCapabilities( modes=["txt2img", "img2img", "inpaint"], - supports_negative_prompt=True, - supports_seed=True, - supports_guidance=True, supports_reference_images=True, - max_images_per_request=1, + max_images_per_request=10, + allowed_aspect_ratios=OPENAI_GPT_IMAGE_ASPECT_RATIOS, + aspect_ratio_sizes=OPENAI_GPT_IMAGE_ASPECT_RATIO_SIZES, ), default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), - panel_schema=ExternalModelPanelSchema(prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}]), + panel_schema=OPENAI_GPT_IMAGE_PANEL_SCHEMA, +) +openai_gpt_image_1 = StarterModel( + name="GPT Image 1", + base=BaseModelType.External, + source="external://openai/gpt-image-1", + description="OpenAI GPT-Image-1 image generation model. High quality image generation. Requires a configured OpenAI API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img", "inpaint"], + supports_reference_images=True, + max_images_per_request=10, + allowed_aspect_ratios=OPENAI_GPT_IMAGE_ASPECT_RATIOS, + aspect_ratio_sizes=OPENAI_GPT_IMAGE_ASPECT_RATIO_SIZES, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=OPENAI_GPT_IMAGE_PANEL_SCHEMA, +) +openai_gpt_image_1_mini = StarterModel( + name="GPT Image 1 Mini", + base=BaseModelType.External, + source="external://openai/gpt-image-1-mini", + description="OpenAI GPT-Image-1-Mini image generation model. Cost-efficient option, 80%% cheaper than GPT-Image-1. Requires a configured OpenAI API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img", "inpaint"], + supports_reference_images=True, + max_images_per_request=10, + allowed_aspect_ratios=OPENAI_GPT_IMAGE_ASPECT_RATIOS, + aspect_ratio_sizes=OPENAI_GPT_IMAGE_ASPECT_RATIO_SIZES, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=OPENAI_GPT_IMAGE_PANEL_SCHEMA, +) +openai_dall_e_3 = StarterModel( + name="DALL-E 3", + base=BaseModelType.External, + source="external://openai/dall-e-3", + description="OpenAI DALL-E 3 image generation model. Supports vivid and natural styles. Only text-to-image, no editing. Requires a configured OpenAI API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img"], + max_images_per_request=1, + allowed_aspect_ratios=["1:1", "7:4", "4:7"], + aspect_ratio_sizes={ + "1:1": ExternalImageSize(width=1024, height=1024), + "7:4": ExternalImageSize(width=1792, height=1024), + "4:7": ExternalImageSize(width=1024, height=1792), + }, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=ExternalModelPanelSchema(image=[{"name": "dimensions"}]), +) +openai_dall_e_2 = StarterModel( + name="DALL-E 2", + base=BaseModelType.External, + source="external://openai/dall-e-2", + description="OpenAI DALL-E 2 image generation model. Supports square images only. Requires a configured OpenAI API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img", "inpaint"], + max_images_per_request=10, + allowed_aspect_ratios=["1:1"], + aspect_ratio_sizes={ + "1:1": ExternalImageSize(width=1024, height=1024), + }, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=ExternalModelPanelSchema(image=[{"name": "dimensions"}]), ) # endregion @@ -1134,7 +1215,11 @@ STARTER_MODELS: list[StarterModel] = [ gemini_flash_image, gemini_pro_image_preview, gemini_3_1_flash_image_preview, + openai_gpt_image_1_5, openai_gpt_image_1, + openai_gpt_image_1_mini, + openai_dall_e_3, + openai_dall_e_2, ] sd1_bundle: list[StarterModel] = [ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts index afdba0212b..7d665a3818 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts @@ -30,7 +30,7 @@ const createExternalConfig = ( panelSchema?: ExternalModelPanelSchema ): ExternalApiModelConfig => { const maxImageSize: ExternalImageSize = { width: 1024, height: 1024 }; - const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024, steps: 30 }; + const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024 }; return { key: 'external-test', @@ -57,21 +57,19 @@ const createExternalConfig = ( }; describe('paramsSlice selectors for external models', () => { - it('uses external capabilities for negative prompt support', () => { + it('returns false for negative prompt support on external models', () => { const config = createExternalConfig({ modes: ['txt2img'], - supports_negative_prompt: true, supports_reference_images: false, }); const model = buildExternalModelIdentifier(config); - expect(selectModelSupportsNegativePrompt.resultFunc(model, config)).toBe(true); + expect(selectModelSupportsNegativePrompt.resultFunc(model)).toBe(false); }); it('uses external capabilities for ref image support', () => { const config = createExternalConfig({ modes: ['txt2img'], - supports_negative_prompt: false, supports_reference_images: false, }); const model = buildExternalModelIdentifier(config); @@ -79,22 +77,19 @@ describe('paramsSlice selectors for external models', () => { expect(selectModelSupportsRefImages.resultFunc(model, config)).toBe(false); }); - it('uses external capabilities for guidance support', () => { + it('returns false for guidance support on external models', () => { const config = createExternalConfig({ modes: ['txt2img'], - supports_negative_prompt: true, supports_reference_images: false, - supports_guidance: true, }); const model = buildExternalModelIdentifier(config); - expect(selectModelSupportsGuidance.resultFunc(model, config)).toBe(true); + expect(selectModelSupportsGuidance.resultFunc(model)).toBe(false); }); it('uses external capabilities for seed support', () => { const config = createExternalConfig({ modes: ['txt2img'], - supports_negative_prompt: true, supports_reference_images: false, supports_seed: false, }); @@ -103,27 +98,22 @@ describe('paramsSlice selectors for external models', () => { expect(selectModelSupportsSeed.resultFunc(model, config)).toBe(false); }); - it('uses external capabilities for steps support', () => { + it('returns false for steps support on external models', () => { const config = createExternalConfig({ modes: ['txt2img'], - supports_negative_prompt: true, supports_reference_images: false, - supports_steps: false, }); const model = buildExternalModelIdentifier(config); - expect(selectModelSupportsSteps.resultFunc(model, config)).toBe(false); + expect(selectModelSupportsSteps.resultFunc(model)).toBe(false); }); it('prefers panel schema over capabilities for control visibility', () => { const config = createExternalConfig( { modes: ['txt2img'], - supports_negative_prompt: true, supports_reference_images: true, - supports_guidance: true, supports_seed: true, - supports_steps: true, }, { prompts: [{ name: 'reference_images' }], @@ -133,11 +123,11 @@ describe('paramsSlice selectors for external models', () => { ); const model = buildExternalModelIdentifier(config); - expect(selectModelSupportsNegativePrompt.resultFunc(model, config)).toBe(false); + expect(selectModelSupportsNegativePrompt.resultFunc(model)).toBe(false); expect(selectModelSupportsRefImages.resultFunc(model, config)).toBe(true); - expect(selectModelSupportsGuidance.resultFunc(model, config)).toBe(false); + expect(selectModelSupportsGuidance.resultFunc(model)).toBe(false); expect(selectModelSupportsSeed.resultFunc(model, config)).toBe(false); - expect(selectModelSupportsSteps.resultFunc(model, config)).toBe(false); + expect(selectModelSupportsSteps.resultFunc(model)).toBe(false); expect(selectModelSupportsDimensions.resultFunc(model, config)).toBe(true); }); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index 87b3789a32..bd41df17f0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -448,6 +448,21 @@ const slice = createSlice({ imageSizeChanged: (state, action: PayloadAction) => { state.imageSize = action.payload; }, + openaiQualityChanged: (state, action: PayloadAction<'auto' | 'high' | 'medium' | 'low'>) => { + state.openaiQuality = action.payload; + }, + openaiBackgroundChanged: (state, action: PayloadAction<'auto' | 'transparent' | 'opaque'>) => { + state.openaiBackground = action.payload; + }, + openaiInputFidelityChanged: (state, action: PayloadAction<'low' | 'high' | null>) => { + state.openaiInputFidelity = action.payload; + }, + geminiTemperatureChanged: (state, action: PayloadAction) => { + state.geminiTemperature = action.payload; + }, + geminiThinkingLevelChanged: (state, action: PayloadAction<'minimal' | 'high' | null>) => { + state.geminiThinkingLevel = action.payload; + }, resolutionPresetSelected: ( state, action: PayloadAction<{ imageSize: string; aspectRatio: string; width: number; height: number }> @@ -593,6 +608,11 @@ export const { resolutionPresetSelected, paramsReset, + openaiQualityChanged, + openaiBackgroundChanged, + openaiInputFidelityChanged, + geminiTemperatureChanged, + geminiThinkingLevelChanged, } = slice.actions; export const paramsSliceConfig: SliceConfig = { @@ -694,22 +714,15 @@ export const selectModelConfig = createSelector( } ); export const selectHasNegativePrompt = createParamsSelector((params) => params.negativePrompt !== null); -export const selectModelSupportsNegativePrompt = createSelector( - selectModel, - selectModelConfig, - (model, modelConfig) => { - if (!model) { - return false; - } - if (modelConfig && isExternalApiModelConfig(modelConfig)) { - return hasExternalPanelControl(modelConfig, 'prompts', 'negative_prompt'); - } - if (model.base === 'external') { - return false; - } - return SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS.includes(model.base); +export const selectModelSupportsNegativePrompt = createSelector(selectModel, (model) => { + if (!model) { + return false; } -); + if (model.base === 'external') { + return false; + } + return SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS.includes(model.base); +}); export const selectModelSupportsRefImages = createSelector(selectModel, selectModelConfig, (model, modelConfig) => { if (!model) { return false; @@ -726,12 +739,12 @@ export const selectModelSupportsOptimizedDenoising = createSelector( selectModel, (model) => !!model && model.base !== 'external' && SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS.includes(model.base) ); -export const selectModelSupportsGuidance = createSelector(selectModel, selectModelConfig, (model, modelConfig) => { +export const selectModelSupportsGuidance = createSelector(selectModel, (model) => { if (!model) { return false; } - if (modelConfig && isExternalApiModelConfig(modelConfig)) { - return hasExternalPanelControl(modelConfig, 'generation', 'guidance'); + if (model.base === 'external') { + return false; } return true; }); @@ -744,12 +757,12 @@ export const selectModelSupportsSeed = createSelector(selectModel, selectModelCo } return true; }); -export const selectModelSupportsSteps = createSelector(selectModel, selectModelConfig, (model, modelConfig) => { +export const selectModelSupportsSteps = createSelector(selectModel, (model) => { if (!model) { return false; } - if (modelConfig && isExternalApiModelConfig(modelConfig)) { - return hasExternalPanelControl(modelConfig, 'generation', 'steps'); + if (model.base === 'external') { + return false; } return true; }); @@ -762,18 +775,6 @@ export const selectModelSupportsDimensions = createSelector(selectModel, selectM } return true; }); -export const selectStepsControl = createSelector(selectModelConfig, (modelConfig) => { - if (modelConfig && isExternalApiModelConfig(modelConfig)) { - return getExternalPanelControl(modelConfig, 'generation', 'steps'); - } - return null; -}); -export const selectGuidanceControl = createSelector(selectModelConfig, (modelConfig) => { - if (modelConfig && isExternalApiModelConfig(modelConfig)) { - return getExternalPanelControl(modelConfig, 'generation', 'guidance'); - } - return null; -}); export const selectSeedControl = createSelector(selectModelConfig, (modelConfig) => { if (modelConfig && isExternalApiModelConfig(modelConfig)) { return getExternalPanelControl(modelConfig, 'image', 'seed'); @@ -847,6 +848,17 @@ export const selectHasFixedDimensionSizes = createSelector( (sizes, presets) => sizes !== null || (presets !== null && presets.length > 0) ); export const selectImageSize = createParamsSelector((params) => params.imageSize); +export const selectOpenaiQuality = createParamsSelector((params) => params.openaiQuality); +export const selectOpenaiBackground = createParamsSelector((params) => params.openaiBackground); +export const selectOpenaiInputFidelity = createParamsSelector((params) => params.openaiInputFidelity); +export const selectGeminiTemperature = createParamsSelector((params) => params.geminiTemperature); +export const selectGeminiThinkingLevel = createParamsSelector((params) => params.geminiThinkingLevel); +export const selectExternalProviderId = createSelector(selectModelConfig, (modelConfig) => { + if (modelConfig && isExternalApiModelConfig(modelConfig)) { + return modelConfig.provider_id; + } + return null; +}); export const selectMainModelConfig = createSelector(selectModelConfig, (modelConfig) => { if (!modelConfig) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 8c62f73b8e..77ad6619db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -758,6 +758,13 @@ export const zParamsState = z.object({ zImageSeedVarianceStrength: z.number().min(0).max(2), zImageSeedVarianceRandomizePercent: z.number().min(1).max(100), imageSize: z.string().nullable().default(null), + // OpenAI-specific external options + openaiQuality: z.enum(['auto', 'high', 'medium', 'low']).default('auto'), + openaiBackground: z.enum(['auto', 'transparent', 'opaque']).default('auto'), + openaiInputFidelity: z.enum(['low', 'high']).nullable().default(null), + // Gemini-specific external options + geminiTemperature: z.number().min(0).max(2).nullable().default(null), + geminiThinkingLevel: z.enum(['minimal', 'high']).nullable().default(null), dimensions: zDimensionsState, }); export type ParamsState = z.infer; @@ -822,6 +829,11 @@ export const getInitialParamsState = (): ParamsState => ({ zImageSeedVarianceStrength: 0.1, zImageSeedVarianceRandomizePercent: 50, imageSize: null, + openaiQuality: 'auto', + openaiBackground: 'auto', + openaiInputFidelity: null, + geminiTemperature: null, + geminiThinkingLevel: null, dimensions: { width: 512, height: 512, diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx index 59fb868a50..aa54071868 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx @@ -224,6 +224,7 @@ const ProviderCard = memo(({ provider, onInstallModels }: ProviderCardProps) => {t('modelManager.externalApiKey')} { - - {t('modelManager.supportsNegativePrompt')} - - {t('modelManager.supportsReferenceImages')} @@ -253,10 +249,6 @@ export const ModelEdit = memo(({ modelConfig }: Props) => { {t('modelManager.supportsSeed')} - - {t('modelManager.supportsGuidance')} - - {t('modelManager.maxImagesPerRequest')} = {}): ExternalApiModelConfig => { const maxImageSize: ExternalImageSize = { width: 1024, height: 1024 }; - const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024, steps: 30 }; + const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024 }; const capabilities: ExternalModelCapabilities = { modes: ['txt2img'], - supports_negative_prompt: true, supports_reference_images: true, supports_seed: true, - supports_guidance: true, - supports_steps: true, max_image_size: maxImageSize, }; @@ -54,7 +51,6 @@ const createExternalModel = (overrides: Partial = {}): E let mockModelConfig: ExternalApiModelConfig | null = null; let mockParams: ParamsState; let mockRefImages: RefImagesState; -let mockPrompts: { positive: string; negative: string }; let mockSizes: { scaledSize: { width: number; height: number } }; const mockOutputFields = { @@ -80,7 +76,6 @@ vi.mock('features/nodes/util/graph/graphBuilderUtils', () => ({ rect: { x: 0, y: 0, width: 512, height: 512 }, }), selectCanvasOutputFields: () => mockOutputFields, - selectPresetModifiedPrompts: () => mockPrompts, })); beforeEach(() => { @@ -88,7 +83,6 @@ beforeEach(() => { steps: 20, guidance: 4.5, } as ParamsState; - mockPrompts = { positive: 'a test prompt', negative: 'bad prompt' }; mockSizes = { scaledSize: { width: 768, height: 512 } }; const imageDTO = { image_name: 'ref.png', width: 64, height: 64 } as ImageDTO; @@ -129,44 +123,14 @@ describe('buildExternalGraph', () => { expect(externalNode?.mode).toBe('txt2img'); expect(externalNode?.width).toBe(768); expect(externalNode?.height).toBe(512); - expect(externalNode?.negative_prompt).toBe('bad prompt'); - expect(externalNode?.steps).toBe(20); - expect(externalNode?.guidance).toBe(4.5); expect((externalNode?.reference_images as Array<{ image_name: string }> | undefined)?.[0]).toEqual({ image_name: 'ref.png', }); - expect(externalNode?.reference_image_weights).toEqual([0.5]); const seedEdge = graph.edges.find((edge) => edge.destination.field === 'seed'); expect(seedEdge).toBeDefined(); }); - it('does not include steps when model does not support them', async () => { - const modelConfig = createExternalModel({ - capabilities: { - modes: ['txt2img'], - supports_negative_prompt: true, - supports_reference_images: true, - supports_seed: true, - supports_guidance: true, - supports_steps: false, - }, - }); - mockModelConfig = modelConfig; - - const { g } = await buildExternalGraph({ - generationMode: 'txt2img', - state: {} as RootState, - manager: null, - }); - const graph = g.getGraph(); - const externalNode = Object.values(graph.nodes).find((node) => node.type === 'openai_image_generation') as - | Record - | undefined; - - expect(externalNode?.steps).toBeNull(); - }); - it('prefers panel schema over capabilities when building node inputs', async () => { const panelSchema: ExternalModelPanelSchema = { prompts: [{ name: 'reference_images' }], @@ -187,9 +151,6 @@ describe('buildExternalGraph', () => { | Record | undefined; - expect(externalNode?.negative_prompt).toBeNull(); - expect(externalNode?.steps).toBeNull(); - expect(externalNode?.guidance).toBeNull(); expect((externalNode?.reference_images as Array<{ image_name: string }> | undefined)?.[0]).toEqual({ image_name: 'ref.png', }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts index 4f686a5986..0ba82234a6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts @@ -8,7 +8,6 @@ import { getOriginalAndScaledSizesForOtherModes, getOriginalAndScaledSizesForTextToImage, selectCanvasOutputFields, - selectPresetModifiedPrompts, } from 'features/nodes/util/graph/graphBuilderUtils'; import { type GraphBuilderArg, @@ -44,13 +43,9 @@ export const buildExternalGraph = async (arg: GraphBuilderArg): Promise config.image) .map((config) => zImageField.parse(config.image?.crop?.image ?? config.image?.original.image)); - const referenceWeights = refImages.entities - .filter((entity) => entity.isEnabled) - .map((entity) => entity.config) - .filter((config) => config.image) - .map((config) => (config.type === 'ip_adapter' ? config.weight : null)); - if (referenceImages.length > 0) { externalNode.reference_images = referenceImages; - if (referenceWeights.every((weight): weight is number => weight !== null)) { - externalNode.reference_image_weights = referenceWeights; - } } } diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx index 1ae1dcdc3a..8fb4e81b59 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx @@ -22,9 +22,7 @@ const createExternalModel = (overrides: Partial = {}): E capabilities: { modes: ['txt2img'], supports_reference_images: false, - supports_negative_prompt: true, supports_seed: true, - supports_guidance: true, max_images_per_request: 1, max_image_size: null, allowed_aspect_ratios: ['1:1', '16:9'], diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx index 80e9d188b3..62fc5aa826 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx @@ -1,8 +1,8 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectGuidance, selectGuidanceControl, setGuidance } from 'features/controlLayers/store/paramsSlice'; -import { memo, useCallback, useMemo } from 'react'; +import { selectGuidance, setGuidance } from 'features/controlLayers/store/paramsSlice'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const CONSTRAINTS = { @@ -23,22 +23,10 @@ export const MARKS = [ const ParamGuidance = () => { const guidance = useAppSelector(selectGuidance); - const externalControl = useAppSelector(selectGuidanceControl); const dispatch = useAppDispatch(); const { t } = useTranslation(); const onChange = useCallback((v: number) => dispatch(setGuidance(v)), [dispatch]); - const sliderMin = externalControl?.slider_min ?? CONSTRAINTS.sliderMin; - const sliderMax = externalControl?.slider_max ?? CONSTRAINTS.sliderMax; - const numberInputMin = externalControl?.number_input_min ?? CONSTRAINTS.numberInputMin; - const numberInputMax = externalControl?.number_input_max ?? CONSTRAINTS.numberInputMax; - const fineStep = externalControl?.fine_step ?? CONSTRAINTS.fineStep; - const coarseStep = externalControl?.coarse_step ?? CONSTRAINTS.coarseStep; - const marks = useMemo( - () => externalControl?.marks ?? [sliderMin, Math.floor(sliderMax - (sliderMax - sliderMin) / 2), sliderMax], - [externalControl?.marks, sliderMin, sliderMax] - ); - return ( @@ -47,20 +35,20 @@ const ParamGuidance = () => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx index b6d810dd2e..31efe5d0a6 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx @@ -1,8 +1,8 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectSteps, selectStepsControl, setSteps } from 'features/controlLayers/store/paramsSlice'; -import { memo, useCallback, useMemo } from 'react'; +import { selectSteps, setSteps } from 'features/controlLayers/store/paramsSlice'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const CONSTRAINTS = { @@ -19,7 +19,6 @@ export const MARKS = [CONSTRAINTS.sliderMin, Math.floor(CONSTRAINTS.sliderMax / const ParamSteps = () => { const steps = useAppSelector(selectSteps); - const externalControl = useAppSelector(selectStepsControl); const dispatch = useAppDispatch(); const { t } = useTranslation(); const onChange = useCallback( @@ -29,17 +28,6 @@ const ParamSteps = () => { [dispatch] ); - const sliderMin = externalControl?.slider_min ?? CONSTRAINTS.sliderMin; - const sliderMax = externalControl?.slider_max ?? CONSTRAINTS.sliderMax; - const numberInputMin = externalControl?.number_input_min ?? CONSTRAINTS.numberInputMin; - const numberInputMax = externalControl?.number_input_max ?? CONSTRAINTS.numberInputMax; - const fineStep = externalControl?.fine_step ?? CONSTRAINTS.fineStep; - const coarseStep = externalControl?.coarse_step ?? CONSTRAINTS.coarseStep; - const marks = useMemo( - () => externalControl?.marks ?? [sliderMin, Math.floor(sliderMax / 2), sliderMax], - [externalControl?.marks, sliderMin, sliderMax] - ); - return ( @@ -48,20 +36,20 @@ const ParamSteps = () => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx index 636260d1d2..0651c47863 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx @@ -22,9 +22,7 @@ const createExternalModel = (overrides: Partial = {}): E capabilities: { modes: ['txt2img'], supports_reference_images: false, - supports_negative_prompt: true, supports_seed: true, - supports_guidance: true, max_images_per_request: 1, max_image_size: null, allowed_aspect_ratios: ['1:1', '16:9'], diff --git a/invokeai/frontend/web/src/features/parameters/components/External/GeminiProviderOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/External/GeminiProviderOptions.tsx new file mode 100644 index 0000000000..1ec27ef809 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/External/GeminiProviderOptions.tsx @@ -0,0 +1,74 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel, Select } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + geminiTemperatureChanged, + geminiThinkingLevelChanged, + selectGeminiTemperature, + selectGeminiThinkingLevel, +} from 'features/controlLayers/store/paramsSlice'; +import type { ChangeEventHandler } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretDownBold } from 'react-icons/pi'; + +const TEMPERATURE_MARKS = [0, 1, 2]; + +export const GeminiProviderOptions = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const temperature = useAppSelector(selectGeminiTemperature); + const thinkingLevel = useAppSelector(selectGeminiThinkingLevel); + + const onTemperatureChange = useCallback((v: number) => dispatch(geminiTemperatureChanged(v)), [dispatch]); + + const onThinkingLevelChange = useCallback>( + (e) => { + const value = e.target.value; + dispatch(geminiThinkingLevelChanged(value === '' ? null : (value as 'minimal' | 'high'))); + }, + [dispatch] + ); + + return ( + <> + + {t('parameters.temperature', 'Temperature')} + + + + + {t('parameters.thinkingLevel', 'Thinking Level')} + + + + ); +}); + +GeminiProviderOptions.displayName = 'GeminiProviderOptions'; diff --git a/invokeai/frontend/web/src/features/parameters/components/External/OpenAIProviderOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/External/OpenAIProviderOptions.tsx new file mode 100644 index 0000000000..f4eba05d84 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/External/OpenAIProviderOptions.tsx @@ -0,0 +1,84 @@ +import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + openaiBackgroundChanged, + openaiInputFidelityChanged, + openaiQualityChanged, + selectOpenaiBackground, + selectOpenaiInputFidelity, + selectOpenaiQuality, +} from 'features/controlLayers/store/paramsSlice'; +import type { ChangeEventHandler } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretDownBold } from 'react-icons/pi'; + +export const OpenAIProviderOptions = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const quality = useAppSelector(selectOpenaiQuality); + const background = useAppSelector(selectOpenaiBackground); + const inputFidelity = useAppSelector(selectOpenaiInputFidelity); + + const onQualityChange = useCallback>( + (e) => dispatch(openaiQualityChanged(e.target.value as 'auto' | 'high' | 'medium' | 'low')), + [dispatch] + ); + + const onBackgroundChange = useCallback>( + (e) => dispatch(openaiBackgroundChanged(e.target.value as 'auto' | 'transparent' | 'opaque')), + [dispatch] + ); + + const onInputFidelityChange = useCallback>( + (e) => { + const value = e.target.value; + dispatch(openaiInputFidelityChanged(value === '' ? null : (value as 'low' | 'high'))); + }, + [dispatch] + ); + + return ( + <> + + {t('parameters.quality', 'Quality')} + + + + {t('parameters.background', 'Background')} + + + + {t('parameters.inputFidelity', 'Input Fidelity')} + + + + ); +}); + +OpenAIProviderOptions.displayName = 'OpenAIProviderOptions'; diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts index b908efa096..7d72b529c2 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts @@ -30,7 +30,6 @@ const createExternalConfig = (modes: ExternalModelCapabilities['modes']): Extern provider_model_id: 'gpt-image-1', capabilities: { modes, - supports_negative_prompt: true, supports_reference_images: false, max_image_size: maxImageSize, }, diff --git a/invokeai/frontend/web/src/features/parameters/util/externalPanelSchema.ts b/invokeai/frontend/web/src/features/parameters/util/externalPanelSchema.ts index 9c1296b1a5..c49190b225 100644 --- a/invokeai/frontend/web/src/features/parameters/util/externalPanelSchema.ts +++ b/invokeai/frontend/web/src/features/parameters/util/externalPanelSchema.ts @@ -11,15 +11,9 @@ type ExternalPanelName = keyof ExternalModelPanelSchema; const buildExternalPanelSchemaFromCapabilities = ( capabilities: ExternalModelCapabilities ): ExternalModelPanelSchema => ({ - prompts: [ - ...(capabilities.supports_negative_prompt ? [{ name: 'negative_prompt' as const }] : []), - ...(capabilities.supports_reference_images ? [{ name: 'reference_images' as const }] : []), - ], + prompts: [...(capabilities.supports_reference_images ? [{ name: 'reference_images' as const }] : [])], image: [{ name: 'dimensions' }, ...(capabilities.supports_seed ? [{ name: 'seed' as const }] : [])], - generation: [ - ...(capabilities.supports_steps ? [{ name: 'steps' as const }] : []), - ...(capabilities.supports_guidance ? [{ name: 'guidance' as const }] : []), - ], + generation: [], }); const getExternalPanelSchema = (modelConfig: ExternalApiModelConfig): ExternalModelPanelSchema => diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx index deb3ea2c83..2988acbd19 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx @@ -1,9 +1,11 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectIsExternal } from 'features/controlLayers/store/paramsSlice'; +import { selectExternalProviderId, selectIsExternal } from 'features/controlLayers/store/paramsSlice'; import { ExternalModelImageSizeSelect } from 'features/parameters/components/Dimensions/ExternalModelImageSizeSelect'; import { ExternalModelResolutionSelect } from 'features/parameters/components/Dimensions/ExternalModelResolutionSelect'; +import { GeminiProviderOptions } from 'features/parameters/components/External/GeminiProviderOptions'; +import { OpenAIProviderOptions } from 'features/parameters/components/External/OpenAIProviderOptions'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,6 +17,7 @@ const formLabelProps: FormLabelProps = { export const ExternalSettingsAccordion = memo(() => { const { t } = useTranslation(); const isExternal = useAppSelector(selectIsExternal); + const providerId = useAppSelector(selectExternalProviderId); const { isOpen, onToggle } = useStandaloneAccordionToggle({ id: 'external-settings', defaultIsOpen: true, @@ -35,6 +38,8 @@ export const ExternalSettingsAccordion = memo(() => { + {providerId === 'openai' && } + {providerId === 'gemini' && } diff --git a/tests/app/invocations/test_external_image_generation.py b/tests/app/invocations/test_external_image_generation.py index 34a2d92162..9cd2919d46 100644 --- a/tests/app/invocations/test_external_image_generation.py +++ b/tests/app/invocations/test_external_image_generation.py @@ -27,7 +27,6 @@ def _build_model() -> ExternalApiModelConfig: capabilities=ExternalModelCapabilities( modes=["txt2img"], supports_reference_images=True, - supports_negative_prompt=True, supports_seed=True, ), ) @@ -57,47 +56,22 @@ def test_external_invocation_builds_request_and_outputs() -> None: model=model_field, mode="txt2img", prompt="A prompt", - negative_prompt="bad", seed=123, num_images=1, width=512, height=512, - steps=10, - guidance=4.5, reference_images=[ImageField(image_name="ref.png")], - reference_image_weights=[0.6], ) output = invocation.invoke(context) request = context._services.external_generation.generate.call_args[0][0] assert request.prompt == "A prompt" - assert request.negative_prompt == "bad" assert request.seed == 123 assert len(request.reference_images) == 1 - assert request.reference_images[0].weight == 0.6 assert output.collection[0].image_name == "result.png" -def test_external_invocation_rejects_mismatched_reference_weights() -> None: - model_config = _build_model() - model_field = ModelIdentifierField.from_config(model_config) - generated_image = Image.new("RGB", (16, 16), color="black") - context = _build_context(model_config, generated_image) - - invocation = ExternalImageGenerationInvocation( - id="external_node", - model=model_field, - mode="txt2img", - prompt="A prompt", - reference_images=[ImageField(image_name="ref.png")], - reference_image_weights=[0.1, 0.2], - ) - - with pytest.raises(ValueError, match="reference_image_weights"): - invocation.invoke(context) - - def test_provider_specific_external_invocation_rejects_wrong_provider() -> None: model_config = _build_model().model_copy(update={"provider_id": "gemini"}) model_field = ModelIdentifierField.from_config(model_config) diff --git a/tests/app/routers/test_model_manager.py b/tests/app/routers/test_model_manager.py index 771790ee9c..3260b539bb 100644 --- a/tests/app/routers/test_model_manager.py +++ b/tests/app/routers/test_model_manager.py @@ -83,9 +83,9 @@ def test_model_manager_external_config_preserves_custom_panel_schema( name="External Custom Schema", provider_id="custom", provider_model_id="custom-model", - capabilities=ExternalModelCapabilities(modes=["txt2img"], supports_negative_prompt=True), + capabilities=ExternalModelCapabilities(modes=["txt2img"]), panel_schema=ExternalModelPanelSchema( - prompts=[{"name": "negative_prompt"}], + prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}], ), source="external://custom/custom-model", @@ -104,7 +104,7 @@ def test_model_manager_external_config_preserves_custom_panel_schema( assert response.status_code == 200 payload = response.json() - assert [control["name"] for control in payload["panel_schema"]["prompts"]] == ["negative_prompt"] + assert [control["name"] for control in payload["panel_schema"]["prompts"]] == ["reference_images"] assert [control["name"] for control in payload["panel_schema"]["image"]] == ["dimensions"] @@ -118,10 +118,7 @@ def test_model_manager_external_starter_model_applies_panel_schema_overrides( provider_model_id="gpt-image-1", capabilities=ExternalModelCapabilities( modes=["txt2img"], - supports_negative_prompt=True, supports_reference_images=False, - supports_guidance=True, - supports_steps=True, ), ) mm2_model_manager.store.add_model(config) diff --git a/tests/app/services/external_generation/test_external_generation_service.py b/tests/app/services/external_generation/test_external_generation_service.py index 4ad0899c9c..927f60b6ee 100644 --- a/tests/app/services/external_generation/test_external_generation_service.py +++ b/tests/app/services/external_generation/test_external_generation_service.py @@ -55,10 +55,8 @@ def _build_request( *, model: ExternalApiModelConfig, mode: str = "txt2img", - negative_prompt: str | None = None, seed: int | None = None, num_images: int = 1, - guidance: float | None = None, width: int = 64, height: int = 64, init_image: Image.Image | None = None, @@ -69,14 +67,11 @@ def _build_request( model=model, mode=mode, # type: ignore[arg-type] prompt="A test prompt", - negative_prompt=negative_prompt, seed=seed, num_images=num_images, width=width, height=height, image_size=None, - steps=10, - guidance=guidance, init_image=init_image, mask_image=mask_image, reference_images=reference_images or [], @@ -117,16 +112,6 @@ def test_generate_validates_mode_support() -> None: service.generate(request) -def test_generate_validates_negative_prompt_support() -> None: - model = _build_model(ExternalModelCapabilities(modes=["txt2img"], supports_negative_prompt=False)) - request = _build_request(model=model, negative_prompt="bad") - provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[])) - service = ExternalGenerationService({"openai": provider}, logging.getLogger("test")) - - with pytest.raises(ExternalProviderCapabilityError, match="Negative prompts"): - service.generate(request) - - def test_generate_requires_init_image_for_img2img() -> None: model = _build_model(ExternalModelCapabilities(modes=["img2img"])) request = _build_request(model=model, mode="img2img") @@ -151,7 +136,7 @@ def test_generate_validates_reference_images() -> None: model = _build_model(ExternalModelCapabilities(modes=["txt2img"], supports_reference_images=False)) request = _build_request( model=model, - reference_images=[ExternalReferenceImage(image=_make_image(), weight=0.8)], + reference_images=[ExternalReferenceImage(image=_make_image())], ) provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[])) service = ExternalGenerationService({"openai": provider}, logging.getLogger("test")) @@ -231,9 +216,9 @@ def test_generate_validates_allowed_aspect_ratios_with_bucket_sizes() -> None: def test_generate_happy_path() -> None: model = _build_model( - ExternalModelCapabilities(modes=["txt2img"], supports_negative_prompt=True, supports_seed=True) + ExternalModelCapabilities(modes=["txt2img"], supports_seed=True) ) - request = _build_request(model=model, negative_prompt="", seed=42) + request = _build_request(model=model, seed=42) result = ExternalGenerationResult(images=[ExternalGeneratedImage(image=_make_image(), seed=42)]) provider = DummyProvider("openai", configured=True, result=result) service = ExternalGenerationService({"openai": provider}, logging.getLogger("test")) diff --git a/tests/app/services/external_generation/test_external_provider_adapters.py b/tests/app/services/external_generation/test_external_provider_adapters.py index a7493ec056..a64f197b4d 100644 --- a/tests/app/services/external_generation/test_external_provider_adapters.py +++ b/tests/app/services/external_generation/test_external_provider_adapters.py @@ -40,10 +40,8 @@ def _build_model(provider_id: str, provider_model_id: str) -> ExternalApiModelCo provider_model_id=provider_model_id, capabilities=ExternalModelCapabilities( modes=["txt2img", "img2img", "inpaint"], - supports_negative_prompt=True, supports_reference_images=True, supports_seed=True, - supports_guidance=True, ), ) @@ -59,14 +57,11 @@ def _build_request( model=model, mode=mode, # type: ignore[arg-type] prompt="A test prompt", - negative_prompt="", seed=123, num_images=1, width=256, height=256, image_size=None, - steps=20, - guidance=5.5, init_image=init_image, mask_image=mask_image, reference_images=reference_images or [], @@ -84,7 +79,7 @@ def test_gemini_generate_success(monkeypatch: pytest.MonkeyPatch) -> None: request = _build_request( model, init_image=init_image, - reference_images=[ExternalReferenceImage(image=ref_image, weight=0.6)], + reference_images=[ExternalReferenceImage(image=ref_image)], ) encoded = encode_image_base64(_make_image("green")) captured: dict[str, object] = {} From 438515bf9a4b66ae95c4aad010414e4541287a76 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Mon, 23 Mar 2026 20:20:01 -0400 Subject: [PATCH 06/56] Chore: Bump version to 6.12.0 (#8981) * chore: bump version to 6.12.0 * chore: update What's New text --- invokeai/frontend/web/public/locales/en.json | 6 +++--- invokeai/version/invokeai_version.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 58be5430a2..0c72fc9510 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -3107,9 +3107,9 @@ "whatsNew": { "whatsNewInInvoke": "What's New in Invoke", "items": [ - "FLUX.2 Klein Support: InvokeAI now supports the new FLUX.2 Klein models (4B and 9B variants) with GGUF, FP8, and Diffusers formats. Features include txt2img, img2img, inpainting, and outpainting. See 'Starter Models' to get started.", - "DyPE support for FLUX models improves high-resolution (>1536 px up to 4K) images. Go to the 'Advanced Options' section to activate.", - "Z-Image Turbo diversity: Active 'Seed Variance Enhancer' under 'Advanced Options' to add diversitiy to your ZiT gens." + "Multi-user mode supports multiple isolated users on the same server.", + "Enhanced support for Z-Image and FLUX.2 Models.", + "Multiple user interface enhancements and new canvas features." ], "takeUserSurvey": "📣 Let us know how you like InvokeAI. Take our User Experience Survey!", "readReleaseNotes": "Read Release Notes", diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index 0f32252c28..50363b269f 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "6.11.1.post1" +__version__ = "6.12.0" From f7aa5fcbbfba1bcdefdda8daa86a27d9e7fbe05b Mon Sep 17 00:00:00 2001 From: Jonathan <34005131+JPPhoto@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:39:52 -0400 Subject: [PATCH 07/56] Add chaining to Collect node (#8933) * Add chained collect node * test(frontend): align parseSchema fixtures with collect v1.1 and normalize undefined fields in assertions * fix(nodes): block collect-to-collect links when inferred item types differ --------- Co-authored-by: Lincoln Stein --- invokeai/app/services/shared/graph.py | 187 ++++++++++++++---- .../store/util/getCollectItemType.test.ts | 10 + .../nodes/store/util/getCollectItemType.ts | 75 +++++-- .../features/nodes/store/util/testUtils.ts | 29 ++- .../store/util/validateConnection.test.ts | 46 +++++ .../nodes/store/util/validateConnection.ts | 18 ++ .../nodes/util/schema/parseSchema.test.ts | 8 +- .../features/nodes/util/schema/parseSchema.ts | 3 - .../frontend/web/src/services/api/schema.ts | 2 +- tests/test_node_graph.py | 113 +++++++++++ tests/test_nodes.py | 13 ++ 11 files changed, 437 insertions(+), 67 deletions(-) diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index fd31448ea4..3ea67a5291 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -123,6 +123,23 @@ def is_any(t: Any) -> bool: return t == Any or Any in get_args(t) +def extract_collection_item_types(t: Any) -> set[Any]: + """Extracts list item types from a collection annotation, including unions containing list branches.""" + if is_any(t): + return {Any} + + if get_origin(t) is list: + return {arg for arg in get_args(t) if arg != NoneType} + + item_types: set[Any] = set() + for arg in get_args(t): + if is_any(arg): + item_types.add(Any) + elif get_origin(arg) is list: + item_types.update(item_arg for item_arg in get_args(arg) if item_arg != NoneType) + return item_types + + def are_connection_types_compatible(from_type: Any, to_type: Any) -> bool: if not from_type or not to_type: return False @@ -280,7 +297,7 @@ class CollectInvocationOutput(BaseInvocationOutput): ) -@invocation("collect", version="1.0.0") +@invocation("collect", version="1.1.0") class CollectInvocation(BaseInvocation): """Collects values into a collection""" @@ -292,7 +309,10 @@ class CollectInvocation(BaseInvocation): input=Input.Connection, ) collection: list[Any] = InputField( - description="The collection, will be provided on execution", default=[], ui_hidden=True + description="An optional collection to append to", + default=[], + ui_type=UIType._Collection, + input=Input.Connection, ) def invoke(self, context: InvocationContext) -> CollectInvocationOutput: @@ -520,7 +540,9 @@ class Graph(BaseModel): # Validate that an edge to this node+field doesn't already exist input_edges = self._get_input_edges(edge.destination.node_id, edge.destination.field) - if len(input_edges) > 0 and not isinstance(to_node, CollectInvocation): + if len(input_edges) > 0 and ( + not isinstance(to_node, CollectInvocation) or edge.destination.field != ITEM_FIELD + ): raise InvalidEdgeError(f"Edge already exists ({edge})") # Validate that no cycles would be created @@ -546,8 +568,10 @@ class Graph(BaseModel): raise InvalidEdgeError(f"Iterator output type does not match iterator input type ({edge}): {err}") # Validate if collector input type matches output type (if this edge results in both being set) - if isinstance(to_node, CollectInvocation) and edge.destination.field == ITEM_FIELD: - err = self._is_collector_connection_valid(edge.destination.node_id, new_input=edge.source) + if isinstance(to_node, CollectInvocation) and edge.destination.field in (ITEM_FIELD, COLLECTION_FIELD): + err = self._is_collector_connection_valid( + edge.destination.node_id, new_input=edge.source, new_input_field=edge.destination.field + ) if err is not None: raise InvalidEdgeError(f"Collector output type does not match collector input type ({edge}): {err}") @@ -676,76 +700,152 @@ class Graph(BaseModel): # Collector input type must match all iterator output types if isinstance(input_node, CollectInvocation): - collector_inputs = self._get_input_edges(input_node.id, ITEM_FIELD) - if len(collector_inputs) == 0: - return "Iterator input collector must have at least one item input edge" - - # Traverse the graph to find the first collector input edge. Collectors validate that their collection - # inputs are all of the same type, so we can use the first input edge to determine the collector's type - first_collector_input_edge = collector_inputs[0] - first_collector_input_type = get_output_field_type( - self.get_node(first_collector_input_edge.source.node_id), first_collector_input_edge.source.field - ) - resolved_collector_type = ( - first_collector_input_type - if get_origin(first_collector_input_type) is None - else get_args(first_collector_input_type) - ) - if not all((are_connection_types_compatible(resolved_collector_type, t) for t in output_field_types)): + input_root_type = self._get_collector_input_root_type(input_node.id) + if input_root_type is None: + return "Iterator input collector must have at least one item or collection input edge" + if not all((are_connection_types_compatible(input_root_type, t) for t in output_field_types)): return "Iterator collection type must match all iterator output types" return None + def _resolve_collector_input_types(self, node_id: str, visited: Optional[set[str]] = None) -> set[Any]: + """Resolves possible item types for a collector's inputs, recursively following chained collectors.""" + visited = visited or set() + if node_id in visited: + return set() + visited.add(node_id) + + input_types: set[Any] = set() + + for edge in self._get_input_edges(node_id, ITEM_FIELD): + input_field_type = get_output_field_type(self.get_node(edge.source.node_id), edge.source.field) + resolved_types = [input_field_type] if get_origin(input_field_type) is None else get_args(input_field_type) + input_types.update(t for t in resolved_types if t != NoneType) + + for edge in self._get_input_edges(node_id, COLLECTION_FIELD): + source_node = self.get_node(edge.source.node_id) + if isinstance(source_node, CollectInvocation) and edge.source.field == COLLECTION_FIELD: + input_types.update(self._resolve_collector_input_types(source_node.id, visited.copy())) + continue + + input_field_type = get_output_field_type(source_node, edge.source.field) + input_types.update(extract_collection_item_types(input_field_type)) + + return input_types + + def _get_collector_input_root_type(self, node_id: str) -> Any | None: + input_types = self._resolve_collector_input_types(node_id) + non_any_input_types = {t for t in input_types if t != Any} + if len(non_any_input_types) == 0 and Any in input_types: + return Any + if len(non_any_input_types) == 0: + return None + + type_tree = nx.DiGraph() + type_tree.add_nodes_from(non_any_input_types) + type_tree.add_edges_from([e for e in itertools.permutations(non_any_input_types, 2) if issubclass(e[1], e[0])]) + type_degrees = type_tree.in_degree(type_tree.nodes) + root_types = [t[0] for t in type_degrees if t[1] == 0] # type: ignore + if len(root_types) != 1: + return Any + return root_types[0] + def _is_collector_connection_valid( self, node_id: str, new_input: Optional[EdgeConnection] = None, + new_input_field: Optional[str] = None, new_output: Optional[EdgeConnection] = None, ) -> str | None: - inputs = [e.source for e in self._get_input_edges(node_id, ITEM_FIELD)] + item_inputs = [e.source for e in self._get_input_edges(node_id, ITEM_FIELD)] + collection_inputs = [e.source for e in self._get_input_edges(node_id, COLLECTION_FIELD)] outputs = [e.destination for e in self._get_output_edges(node_id, COLLECTION_FIELD)] if new_input is not None: - inputs.append(new_input) + field = new_input_field or ITEM_FIELD + if field == ITEM_FIELD: + item_inputs.append(new_input) + elif field == COLLECTION_FIELD: + collection_inputs.append(new_input) if new_output is not None: outputs.append(new_output) - # Get input and output fields (the fields linked to the iterator's input/output) - input_field_types = [get_output_field_type(self.get_node(e.node_id), e.field) for e in inputs] + if len(item_inputs) == 0 and len(collection_inputs) == 0: + return "Collector must have at least one item or collection input edge" + + # Get input and output fields (the fields linked to the collector's input/output) + item_input_field_types = [get_output_field_type(self.get_node(e.node_id), e.field) for e in item_inputs] + collection_input_field_types = [ + get_output_field_type(self.get_node(e.node_id), e.field) for e in collection_inputs + ] output_field_types = [get_input_field_type(self.get_node(e.node_id), e.field) for e in outputs] + if not all((is_list_or_contains_list(t) or is_any(t) for t in collection_input_field_types)): + return "Collector collection input must be a collection" + # Validate that all inputs are derived from or match a single type input_field_types = { resolved_type - for input_field_type in input_field_types + for input_field_type in item_input_field_types for resolved_type in ( [input_field_type] if get_origin(input_field_type) is None else get_args(input_field_type) ) if resolved_type != NoneType } # Get unique types + + for input_conn, input_field_type in zip(collection_inputs, collection_input_field_types, strict=False): + source_node = self.get_node(input_conn.node_id) + if isinstance(source_node, CollectInvocation) and input_conn.field == COLLECTION_FIELD: + input_field_types.update(self._resolve_collector_input_types(source_node.id)) + continue + input_field_types.update(extract_collection_item_types(input_field_type)) + + non_any_input_field_types = {t for t in input_field_types if t != Any} type_tree = nx.DiGraph() - type_tree.add_nodes_from(input_field_types) - type_tree.add_edges_from([e for e in itertools.permutations(input_field_types, 2) if issubclass(e[1], e[0])]) + type_tree.add_nodes_from(non_any_input_field_types) + type_tree.add_edges_from( + [e for e in itertools.permutations(non_any_input_field_types, 2) if issubclass(e[1], e[0])] + ) type_degrees = type_tree.in_degree(type_tree.nodes) - if sum((t[1] == 0 for t in type_degrees)) != 1: # type: ignore + root_types = [t[0] for t in type_degrees if t[1] == 0] # type: ignore + if len(root_types) > 1: return "Collector input collection items must be of a single type" - # Get the input root type - input_root_type = next(t[0] for t in type_degrees if t[1] == 0) # type: ignore + # Get the input root type (if known) + input_root_type = root_types[0] if len(root_types) == 1 else None # Verify that all outputs are lists if not all(is_list_or_contains_list(t) or is_any(t) for t in output_field_types): return "Collector output must connect to a collection input" # Verify that all outputs match the input type (are a base class or the same class) - if not all( - is_any(t) - or is_union_subtype(input_root_type, get_args(t)[0]) - or issubclass(input_root_type, get_args(t)[0]) - for t in output_field_types - ): + if input_root_type is not None: + if not all( + is_any(t) + or is_union_subtype(input_root_type, get_args(t)[0]) + or issubclass(input_root_type, get_args(t)[0]) + for t in output_field_types + ): + return "Collector outputs must connect to a collection input with a matching type" + elif any(not is_any(t) and get_args(t)[0] != Any for t in output_field_types): return "Collector outputs must connect to a collection input with a matching type" + # If this collector outputs to another collector's collection input, validate against the downstream + # collector's resolved input type (if available). + for output in outputs: + output_node = self.get_node(output.node_id) + if not isinstance(output_node, CollectInvocation) or output.field != COLLECTION_FIELD: + continue + output_root_type = self._get_collector_input_root_type(output_node.id) + if output_root_type is None: + continue + if input_root_type is None: + if output_root_type != Any: + return "Collector outputs must connect to a collection input with a matching type" + continue + if not are_connection_types_compatible(input_root_type, output_root_type): + return "Collector outputs must connect to a collection input with a matching type" + return None def nx_graph(self) -> nx.DiGraph: @@ -1211,8 +1311,19 @@ class GraphExecutionState(BaseModel): if isinstance(node, CollectInvocation): item_edges = [e for e in input_edges if e.destination.field == ITEM_FIELD] item_edges.sort(key=lambda e: (self._get_iteration_path(e.source.node_id), e.source.node_id)) + collection_edges = [e for e in input_edges if e.destination.field == COLLECTION_FIELD] + collection_edges.sort(key=lambda e: (self._get_iteration_path(e.source.node_id), e.source.node_id)) - output_collection = [copydeep(getattr(self.results[e.source.node_id], e.source.field)) for e in item_edges] + output_collection = [] + for edge in collection_edges: + source_value = copydeep(getattr(self.results[edge.source.node_id], edge.source.field)) + if isinstance(source_value, list): + output_collection.extend(source_value) + else: + output_collection.append(source_value) + output_collection.extend( + copydeep(getattr(self.results[e.source.node_id], e.source.field)) for e in item_edges + ) node.collection = output_collection else: for edge in input_edges: diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts index 8adc013ab9..fb4d7ee48c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.test.ts @@ -41,4 +41,14 @@ describe(getCollectItemType.name, () => { const result = getCollectItemType({ add: addWithoutOutputValue, collect }, [n2, n1], [e1], n1.id); expect(result).toBeNull(); }); + + it('should return the upstream collect item type for chained collects', () => { + const n1 = buildNode(collect); + const n2 = buildNode(collect); + const n3 = buildNode(add); + const e1 = buildEdge(n3.id, 'value', n1.id, 'item'); + const e2 = buildEdge(n1.id, 'collection', n2.id, 'collection'); + const result = getCollectItemType(templates, [n1, n2, n3], [e1, e2], n2.id); + expect(result).toEqual({ name: 'IntegerField', cardinality: 'SINGLE', batch: false }); + }); }); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts index 9fb2795ae8..35ec20220e 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/getCollectItemType.ts @@ -2,6 +2,16 @@ import type { Templates } from 'features/nodes/store/types'; import type { FieldType } from 'features/nodes/types/field'; import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation'; +const toItemType = (fieldType: FieldType): FieldType | null => { + if (fieldType.name === 'CollectionField') { + return null; + } + if (fieldType.cardinality === 'COLLECTION' || fieldType.cardinality === 'SINGLE_OR_COLLECTION') { + return { ...fieldType, cardinality: 'SINGLE' }; + } + return fieldType; +}; + /** * Given a collect node, return the type of the items it collects. The graph is traversed to find the first node and * field connected to the collector's `item` input. The field type of that field is returned, else null if there is no @@ -18,21 +28,56 @@ export const getCollectItemType = ( edges: AnyEdge[], nodeId: string ): FieldType | null => { - const firstEdgeToCollect = edges.find((edge) => edge.target === nodeId && edge.targetHandle === 'item'); - if (!firstEdgeToCollect?.sourceHandle) { + const getCollectItemTypeInternal = (currentNodeId: string, visited: Set): FieldType | null => { + if (visited.has(currentNodeId)) { + return null; + } + visited.add(currentNodeId); + + const firstItemEdgeToCollect = edges.find((edge) => edge.target === currentNodeId && edge.targetHandle === 'item'); + if (firstItemEdgeToCollect?.sourceHandle) { + const node = nodes.find((n) => n.id === firstItemEdgeToCollect.source); + if (!node) { + return null; + } + const template = templates[node.data.type]; + if (!template) { + return null; + } + const fieldTemplate = template.outputs[firstItemEdgeToCollect.sourceHandle]; + if (!fieldTemplate) { + return null; + } + return toItemType(fieldTemplate.type); + } + + const firstCollectionEdgeToCollect = edges.find( + (edge) => edge.target === currentNodeId && edge.targetHandle === 'collection' + ); + if (!firstCollectionEdgeToCollect?.sourceHandle) { + return null; + } + const sourceNode = nodes.find((n) => n.id === firstCollectionEdgeToCollect.source); + if (!sourceNode) { + return null; + } + if (sourceNode.data.type === 'collect' && firstCollectionEdgeToCollect.sourceHandle === 'collection') { + return getCollectItemTypeInternal(sourceNode.id, visited); + } + const sourceTemplate = templates[sourceNode.data.type]; + if (!sourceTemplate) { + return null; + } + const sourceFieldTemplate = sourceTemplate.outputs[firstCollectionEdgeToCollect.sourceHandle]; + if (!sourceFieldTemplate) { + return null; + } + return toItemType(sourceFieldTemplate.type); + }; + + const itemType = getCollectItemTypeInternal(nodeId, new Set()); + if (!itemType) { return null; } - const node = nodes.find((n) => n.id === firstEdgeToCollect.source); - if (!node) { - return null; - } - const template = templates[node.data.type]; - if (!template) { - return null; - } - const fieldTemplate = template.outputs[firstEdgeToCollect.sourceHandle]; - if (!fieldTemplate) { - return null; - } - return fieldTemplate.type; + return itemType; }; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts index 7442561984..1eb445beaf 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts @@ -133,11 +133,27 @@ export const sub: InvocationTemplate = { export const collect: InvocationTemplate = { title: 'Collect', type: 'collect', - version: '1.0.0', + version: '1.1.0', tags: [], description: 'Collects values into a collection', outputType: 'collect_output', inputs: { + collection: { + name: 'collection', + title: 'Collection', + required: false, + default: undefined, + description: 'An optional collection to append to', + fieldKind: 'input', + input: 'connection', + ui_hidden: false, + ui_type: 'CollectionField' as const, + type: { + name: 'CollectionField' as const, + cardinality: 'COLLECTION', + batch: false, + }, + }, item: { name: 'item', title: 'Collection Item', @@ -1162,13 +1178,12 @@ export const schema = { items: {}, type: 'array', title: 'Collection', - description: 'The collection, will be provided on execution', - default: [], + description: 'An optional collection to append to', field_kind: 'input', - input: 'any', - orig_default: [], + input: 'connection', orig_required: false, - ui_hidden: true, + ui_hidden: false, + ui_type: 'CollectionField', }, type: { type: 'string', @@ -1185,7 +1200,7 @@ export const schema = { node_pack: 'invokeai', description: 'Collects values into a collection', classification: 'stable', - version: '1.0.0', + version: '1.1.0', output: { $ref: '#/components/schemas/CollectInvocationOutput', }, diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts index 4108f57c07..947d8745f0 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts @@ -122,6 +122,52 @@ describe(validateConnection.name, () => { expect(r).toEqual(null); }); + it('should accept chaining collect collection output to collect collection input', () => { + const n1 = buildNode(collect); + const n2 = buildNode(collect); + const nodes = [n1, n2]; + const c = { source: n1.id, sourceHandle: 'collection', target: n2.id, targetHandle: 'collection' }; + const r = validateConnection(c, nodes, [], templates, null); + expect(r).toEqual(null); + }); + + it('should reject multiple connections to collect collection input', () => { + const n1 = buildNode(collect); + const n2 = buildNode(collect); + const n3 = buildNode(collect); + const nodes = [n1, n2, n3]; + const e1 = buildEdge(n1.id, 'collection', n2.id, 'collection'); + const c = { source: n3.id, sourceHandle: 'collection', target: n2.id, targetHandle: 'collection' }; + const r = validateConnection(c, nodes, [e1], templates, null); + expect(r).toEqual('nodes.inputMayOnlyHaveOneConnection'); + }); + + it('should reject mismatched item connection when collect is typed via chained collection', () => { + const n1 = buildNode(add); + const n2 = buildNode(collect); + const n3 = buildNode(collect); + const n4 = buildNode(main_model_loader); + const nodes = [n1, n2, n3, n4]; + const e1 = buildEdge(n1.id, 'value', n2.id, 'item'); + const e2 = buildEdge(n2.id, 'collection', n3.id, 'collection'); + const c = { source: n4.id, sourceHandle: 'vae', target: n3.id, targetHandle: 'item' }; + const r = validateConnection(c, nodes, [e1, e2], templates, null); + expect(r).toEqual('nodes.cannotMixAndMatchCollectionItemTypes'); + }); + + it('should reject chaining collection-to-collection for differently typed collects', () => { + const n1 = buildNode(add); + const n2 = buildNode(img_resize); + const n3 = buildNode(collect); + const n4 = buildNode(collect); + const nodes = [n1, n2, n3, n4]; + const e1 = buildEdge(n1.id, 'value', n3.id, 'item'); + const e2 = buildEdge(n2.id, 'image', n4.id, 'item'); + const c = { source: n3.id, sourceHandle: 'collection', target: n4.id, targetHandle: 'collection' }; + const r = validateConnection(c, nodes, [e1, e2], templates, null); + expect(r).toEqual('nodes.cannotMixAndMatchCollectionItemTypes'); + }); + it('should reject connections to target field that is already connected', () => { const n1 = buildNode(add); const n2 = buildNode(add); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts index aaeb10edfd..9024a16f42 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts @@ -108,6 +108,24 @@ export const validateConnection: ValidateConnectionFunc = ( } } + if ( + sourceNode.data.type === 'collect' && + c.sourceHandle === 'collection' && + targetNode.data.type === 'collect' && + c.targetHandle === 'collection' + ) { + // Chained collect nodes should preserve a single item type when both ends are already typed. + const sourceCollectItemType = getCollectItemType(templates, nodes, edges, sourceNode.id); + const targetCollectItemType = getCollectItemType(templates, nodes, edges, targetNode.id); + if ( + sourceCollectItemType && + targetCollectItemType && + !areTypesEqual(sourceCollectItemType, targetCollectItemType) + ) { + return 'nodes.cannotMixAndMatchCollectionItemTypes'; + } + } + if (filteredEdges.find(getTargetEqualityPredicate(c))) { // CollectionItemField inputs can have multiple input connections if (targetFieldTemplate.type.name !== 'CollectionItemField') { diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts index c9dad573a9..45dd0f79a3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.test.ts @@ -3,17 +3,19 @@ import { schema, templates } from 'features/nodes/store/util/testUtils'; import { parseSchema } from 'features/nodes/util/schema/parseSchema'; import { describe, expect, it } from 'vitest'; +const stripUndefinedDeep = (value: T): T => JSON.parse(JSON.stringify(value)) as T; + describe('parseSchema', () => { it('should parse the schema', () => { const parsed = parseSchema(schema); - expect(parsed).toEqual(templates); + expect(stripUndefinedDeep(parsed)).toEqual(stripUndefinedDeep(templates)); }); it('should omit denied nodes', () => { const parsed = parseSchema(schema, undefined, ['add']); - expect(parsed).toEqual(omit(templates, 'add')); + expect(stripUndefinedDeep(parsed)).toEqual(stripUndefinedDeep(omit(templates, 'add'))); }); it('should include only allowed nodes', () => { const parsed = parseSchema(schema, ['add']); - expect(parsed).toEqual(pick(templates, 'add')); + expect(stripUndefinedDeep(parsed)).toEqual(stripUndefinedDeep(pick(templates, 'add'))); }); }); diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts index 1371db1568..57cd9943c5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts @@ -39,9 +39,6 @@ const isReservedInputField = (nodeType: string, fieldName: string) => { if (RESERVED_INPUT_FIELD_NAMES.includes(fieldName)) { return true; } - if (nodeType === 'collect' && fieldName === 'collection') { - return true; - } if (nodeType === 'iterate' && fieldName === 'index') { return true; } diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index fc6506ce22..895bc6f36a 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -4988,7 +4988,7 @@ export type components = { item?: unknown | null; /** * Collection - * @description The collection, will be provided on execution + * @description An optional collection to append to * @default [] */ collection?: unknown[]; diff --git a/tests/test_node_graph.py b/tests/test_node_graph.py index 160dc96d85..4f3b262204 100644 --- a/tests/test_node_graph.py +++ b/tests/test_node_graph.py @@ -40,6 +40,7 @@ from tests.test_nodes import ( PromptTestInvocation, PromptTestInvocationOutput, TextToImageTestInvocation, + UnionCollectionTestInvocation, get_single_output_from_session, run_session_with_mock_context, ) @@ -337,6 +338,100 @@ def test_graph_collector_invalid_with_non_list_output(): g.add_edge(e3) +def test_graph_collector_can_chain_collection_input(): + g = Graph() + n1 = PromptCollectionTestInvocation(id="1", collection=["Banana", "Sushi"]) + n2 = PromptTestInvocation(id="2", prompt="Ramen") + n3 = CollectInvocation(id="3") + g.add_node(n1) + g.add_node(n2) + g.add_node(n3) + + g.add_edge(create_edge("1", "collection", "3", "collection")) + g.add_edge(create_edge("2", "prompt", "3", "item")) + + session = GraphExecutionState(graph=g) + run_session_with_mock_context(session) + output = get_single_output_from_session(session, n3.id) + + assert isinstance(output, CollectInvocationOutput) + assert output.collection == ["Banana", "Sushi", "Ramen"] + + +def test_graph_collector_chain_rejects_mismatched_item_type(): + g = Graph() + n1 = PromptCollectionTestInvocation(id="1", collection=["Banana", "Sushi"]) + n2 = IntegerInvocation(id="2", value=7) + n3 = CollectInvocation(id="3") + g.add_node(n1) + g.add_node(n2) + g.add_node(n3) + + g.add_edge(create_edge("1", "collection", "3", "collection")) + with pytest.raises(InvalidEdgeError): + g.add_edge(create_edge("2", "value", "3", "item")) + + +def test_graph_iterator_accepts_collector_chained_collection_input(): + g = Graph() + n1 = PromptTestInvocation(id="1", prompt="Banana") + n2 = CollectInvocation(id="2") + n3 = CollectInvocation(id="3") + n4 = IterateInvocation(id="4") + n5 = PromptTestInvocation(id="5") + g.add_node(n1) + g.add_node(n2) + g.add_node(n3) + g.add_node(n4) + g.add_node(n5) + + g.add_edge(create_edge("1", "prompt", "2", "item")) + g.add_edge(create_edge("2", "collection", "3", "collection")) + g.add_edge(create_edge("3", "collection", "4", "collection")) + g.add_edge(create_edge("4", "item", "5", "prompt")) + + session = GraphExecutionState(graph=g) + run_session_with_mock_context(session) + + output = get_single_output_from_session(session, n5.id) + assert isinstance(output, PromptTestInvocationOutput) + assert output.prompt == "Banana" + + +def test_graph_collector_chain_rejects_upstream_mismatch_added_late(): + g = Graph() + n1 = CollectInvocation(id="1") + n2 = CollectInvocation(id="2") + n3 = PromptTestInvocation(id="3", prompt="typed-as-string") + n4 = ColorInvocation(id="4") + g.add_node(n1) + g.add_node(n2) + g.add_node(n3) + g.add_node(n4) + + # Connect chain first while n1 is still untyped. + g.add_edge(create_edge("1", "collection", "2", "collection")) + # Constrain downstream collector to strings. + g.add_edge(create_edge("3", "prompt", "2", "item")) + # Now adding an incompatible type to the upstream collector must fail. + with pytest.raises(InvalidEdgeError): + g.add_edge(create_edge("4", "color", "1", "item")) + + +def test_graph_collector_rejects_mismatched_item_with_union_collection_input(): + g = Graph() + n1 = UnionCollectionTestInvocation(id="1") + n2 = CollectInvocation(id="2") + n3 = ColorInvocation(id="3") + g.add_node(n1) + g.add_node(n2) + g.add_node(n3) + + g.add_edge(create_edge("1", "value", "2", "collection")) + with pytest.raises(InvalidEdgeError): + g.add_edge(create_edge("3", "color", "2", "item")) + + def test_graph_connects_iterator(): g = Graph() n1 = ListPassThroughInvocation(id="1") @@ -712,6 +807,24 @@ def test_iterate_accepts_collection(): g.add_edge(e3) +def test_iterate_accepts_collection_from_any_only_collector(): + g = Graph() + n1 = AnyTypeTestInvocation(id="1") + n2 = CollectInvocation(id="2") + n3 = IterateInvocation(id="3") + n4 = AnyTypeTestInvocation(id="4") + g.add_node(n1) + g.add_node(n2) + g.add_node(n3) + g.add_node(n4) + e1 = create_edge(n1.id, "value", n2.id, "item") + e2 = create_edge(n2.id, "collection", n3.id, "collection") + e3 = create_edge(n3.id, "item", n4.id, "value") + g.add_edge(e1) + g.add_edge(e2) + g.add_edge(e3) + + def test_iterate_validates_collection_inputs_against_iterator_outputs(): g = Graph() n1 = IntegerInvocation(id="1", value=1) diff --git a/tests/test_nodes.py b/tests/test_nodes.py index 04ea5126f0..6e8d25a603 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -107,6 +107,19 @@ class PolymorphicStringTestInvocation(BaseInvocation): return PromptCollectionTestInvocationOutput(collection=self.value) +@invocation_output("test_union_collection_output") +class UnionCollectionTestInvocationOutput(BaseInvocationOutput): + value: Union[str, list[str], None] = OutputField(default=None) + + +@invocation("test_union_collection", version="1.0.0") +class UnionCollectionTestInvocation(BaseInvocation): + value: Union[str, list[str], None] = InputField(default=None) + + def invoke(self, context: InvocationContext) -> UnionCollectionTestInvocationOutput: + return UnionCollectionTestInvocationOutput(value=self.value) + + # Importing these must happen after test invocations are defined or they won't register from invokeai.app.services.events.events_base import EventServiceBase # noqa: E402 from invokeai.app.services.shared.graph import Edge, EdgeConnection, GraphExecutionState # noqa: E402 From d32f6b5a5650364accbcf762ff16bd30991414d0 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 24 Mar 2026 04:09:57 +0100 Subject: [PATCH 08/56] ui: translations update from weblate (#8985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * translationBot(ui): update translation (Italian) Currently translated at 98.0% (2205 of 2250 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI * translationBot(ui): update translation files Updated by "Remove blank strings" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ Translation: InvokeAI/Web UI * translationBot(ui): update translation (Italian) Currently translated at 97.8% (2210 of 2259 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.8% (2224 of 2272 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 98.1% (2252 of 2295 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 98.0% (2264 of 2309 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Russian) Currently translated at 60.7% (1419 of 2334 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/ * translationBot(ui): update translation (Italian) Currently translated at 98.1% (2290 of 2334 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.7% (2319 of 2372 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.7% (2327 of 2380 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.7% (2328 of 2382 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.5% (2370 of 2429 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Finnish) Currently translated at 1.5% (37 of 2429 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fi/ * translationBot(ui): update translation (Italian) Currently translated at 97.5% (2373 of 2433 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Japanese) Currently translated at 87.1% (2120 of 2433 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/ * translationBot(ui): update translation (Italian) Currently translated at 97.5% (2374 of 2433 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ --------- Co-authored-by: Riccardo Giovanetti Co-authored-by: DustyShoe Co-authored-by: Ilmari Laakkonen Co-authored-by: 嶋田豪介 --- invokeai/frontend/web/public/locales/it.json | 5 +- invokeai/frontend/web/public/locales/ja.json | 286 ++++++++++--------- 2 files changed, 148 insertions(+), 143 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 7a6dafe4c7..cafd91d2a4 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -2639,7 +2639,7 @@ "desc": "Seleziona un singolo oggetto di destinazione. Una volta completata la selezione, fai clic su Applica per eliminare tutto ciò che si trova al di fuori dell'area selezionata, oppure salva la selezione come nuovo livello.", "visualModeDesc": "La modalità visiva utilizza input di tipo riquadro e punto per selezionare un oggetto.", "visualMode1": "Fai clic e trascina per disegnare un riquadro attorno all'oggetto che desideri selezionare. Puoi ottenere risultati migliori disegnando il riquadro un po' più grande o più piccolo dell'oggetto.", - "visualMode2": "Fare clic per aggiungere un punto di iinclusionei verde oppure fare clic tenendo premuto Maiusc per aggiungere un punto di iesclusionei rosso per indicare al modello cosa includere o escludere.", + "visualMode2": "Fai clic per aggiungere un punto verde includi oppure fai clic tenendo premuto il tasto Maiusc per aggiungere un punto rosso escludi per indicare al modello cosa includere o escludere.", "visualMode3": "I punti possono essere utilizzati per perfezionare una selezione di caselle oppure in modo indipendente.", "promptModeDesc": "La modalità Prompt utilizza l'input di testo per selezionare un oggetto.", "promptMode1": "Digitare una breve descrizione dell'oggetto che si desidera selezionare.", @@ -3089,7 +3089,8 @@ "passwordsDoNotMatch": "Le password non corrispondono", "createAccount": "Crea un account amministratore", "creatingAccount": "Impostazione in corso...", - "setupFailed": "Installazione non riuscita. Riprova." + "setupFailed": "Installazione non riuscita. Riprova.", + "passwordHelperRelaxed": "Inserisci una password qualsiasi (verrà visualizzata la sua robustezza)" }, "userMenu": "Menu utente", "logout": "Esci", diff --git a/invokeai/frontend/web/public/locales/ja.json b/invokeai/frontend/web/public/locales/ja.json index 291b34cafa..7acf6c98ca 100644 --- a/invokeai/frontend/web/public/locales/ja.json +++ b/invokeai/frontend/web/public/locales/ja.json @@ -8,7 +8,7 @@ "back": "戻る", "statusDisconnected": "切断済", "cancel": "キャンセル", - "accept": "同意", + "accept": "確定", "img2img": "img2img", "loading": "ロード中", "githubLabel": "Github", @@ -33,9 +33,9 @@ "batch": "バッチマネージャー", "advanced": "高度", "created": "作成済", - "green": "緑", - "blue": "青", - "alpha": "アルファ", + "green": "G", + "blue": "B", + "alpha": "α", "outpaint": "outpaint", "unknown": "不明", "updated": "更新済", @@ -44,7 +44,7 @@ "copyError": "$t(gallery.copy) エラー", "data": "データ", "template": "テンプレート", - "red": "赤", + "red": "R", "or": "または", "checkpoint": "Checkpoint", "direction": "方向", @@ -194,7 +194,7 @@ "assets": "アセット", "useForPromptGeneration": "プロンプト生成に使用する", "jump": "ジャンプ", - "noImagesInGallery": "ディスプレイに画像がありません", + "noImagesInGallery": "表示する画像がありません", "unableToLoad": "ギャラリーを読み込めません", "selectAnImageToCompare": "比較する画像を選択", "openViewer": "ビューアーを開く", @@ -211,7 +211,7 @@ }, "useSize": { "title": "サイズを使用", - "desc": "現画像のサイズをbboxサイズとして使用する." + "desc": "現画像のサイズをバウンディングボックスのサイズとして使用する." }, "recallPrompts": { "title": "プロンプトを再使用", @@ -366,8 +366,8 @@ "desc": "矩形ツールを選択します。" }, "settings": { - "behavior": "行動", - "display": "ディスプレイ", + "behavior": "挙動", + "display": "表示", "grid": "グリッド", "debug": "デバッグ" }, @@ -388,25 +388,25 @@ "desc": "選択したインペイント マスクを反転し、反対の透明度を持つ新しいマスクを作成します。" }, "fitBboxToLayers": { - "title": "Bboxをレイヤーに合わせる", - "desc": "表示レイヤーに合わせて生成境界ボックスを自動的に調整します" + "title": "バウンディングボックスをレイヤー群に合わせる", + "desc": "表示されているレイヤーに合わせて生成バウンディングボックスを自動的に調整します" }, "fitBboxToMasks": { - "title": "Bboxをマスクにフィットさせる", - "desc": "目に見えるインペイントマスクに合わせて生成境界ボックスを自動的に調整します" + "title": "バウンディングボックスをマスクにフィットさせる", + "desc": "可視のインペイントマスクに合わせて生成バウンディングボックスを自動的に調整します" }, "toggleBbox": { - "title": "Bboxの表示/非表示を切り替える", - "desc": "生成境界ボックスを非表示または表示する" + "title": "バウンディングボックスの表示/非表示を切り替える", + "desc": "生成バウンディングボックスを非表示または表示する" }, "applySegmentAnything": { - "title": "何でもセグメント化を適用する", - "desc": "現在の「何でもセグメント」マスクを適用します。", + "title": "Segment Anythingを適用する", + "desc": "現在のSegment Anythingマスクを適用します。", "key": "入力" }, "cancelSegmentAnything": { "title": "セグメントをキャンセル", - "desc": "現在の「何でもセグメント」操作をキャンセルします。", + "desc": "現在のSegment Anything操作をキャンセルします。", "key": "エスケープ" } }, @@ -468,8 +468,8 @@ "title": "キャンバスタブを選択" }, "selectUpscalingTab": { - "desc": "アップスケーリングタブを選択します。", - "title": "アップスケーリングタブを選択" + "desc": "アップスケールタブを選択します。", + "title": "アップスケールタブを選択" }, "toggleRightPanel": { "desc": "右パネルを表示または非表示。", @@ -504,7 +504,7 @@ "desc": "カーソルをポジティブプロンプト欄に移動します。" }, "promptHistoryPrev": { - "title": "履歴の前のプロンプト", + "title": "ヒストリーの以前のプロンプト", "desc": "プロンプトにフォーカスがある場合は、履歴内の前の(古い)プロンプトに移動します。" }, "promptHistoryNext": { @@ -636,9 +636,9 @@ "controlLora": "コントロールLoRA", "triggerPhrases": "トリガーフレーズ", "t5Encoder": "T5エンコーダー", - "textualInversions": "テキスト反転", + "textualInversions": "Textual Inversions", "fluxRedux": "FLUX リダックス", - "installQueue": "キューをインストール", + "installQueue": "インストール進捗状況", "noMatchingModels": "マッチするモデルがありません", "noDefaultSettings": "このモデルには構成されたデフォルト設定がありません.デフォルト設定を追加するためにモデルマネージャーにアクセスしてください.", "usingDefaultSettings": "モデルのデフォルト設定を使用する", @@ -651,7 +651,7 @@ "main": "メイン", "defaultSettings": "デフォルト設定", "deleteModelImage": "モデル画像を削除", - "hfTokenInvalid": "ハギングフェイストークンが無効または見つかりません", + "hfTokenInvalid": "HuggingFaceトークンが無効または見つかりません", "hfForbiddenErrorMessage": "リポジトリにアクセスすることを勧めます.所有者はダウンロードにあたり利用規約への同意を要求する場合があります.", "noModelsInstalled": "インストールされているモデルがありません", "pathToConfig": "設定へのパス", @@ -665,8 +665,8 @@ "installRepo": "リポジトリをインストール", "localOnly": "ローカルのみ", "huggingFaceHelper": "いくつかのモデルがこのリポジトリで見つかった場合,1つを選択してインストールするように求められます.", - "hfTokenInvalidErrorMessage": "ハギングフェイストークンが無効または見つかりません.", - "hfTokenRequired": "有効なハギングフェイストークンが必要なモデルをダウンロードしようとしています.", + "hfTokenInvalidErrorMessage": "HuggingFaceトークンが無効または見つかりません。", + "hfTokenRequired": "有効なHuggingFaceトークンが必要なモデルをダウンロードしようとしています。", "hfTokenInvalidErrorMessage2": "更新してください ", "modelImageDeleted": "モデル画像削除", "repoVariant": "リポジトリバリアント", @@ -679,17 +679,17 @@ "urlOrLocalPath": "URLかローカルパス", "clipLEmbed": "クリップ-L 埋め込み", "defaultSettingsSaved": "デフォルト設定を保存しました", - "hfTokenUnableToVerify": "ハギングフェイストークンを確認できません", - "hfForbidden": "このハギングフェイスモデルにアクセスできません", - "hfTokenLabel": "ハギングフェイストークン(いくつかのモデルに必要)", + "hfTokenUnableToVerify": "HuggingFaceトークンを確認できません", + "hfForbidden": "このHuggingFaceモデルにアクセスできません", + "hfTokenLabel": "HuggingFaceトークン(いくつかのモデルに必要)", "noModelSelected": "モデルが選択されていません", "prune": "除去", - "hfTokenHelperText": "いくつかのモデルにハギングフェイストークンが必要です.ここをクリックしてあなたのトークンを作成してください.", + "hfTokenHelperText": "いくつかのモデルにHuggingFaceトークンが必要です。ここをクリックしてあなたのトークンを作成してください。", "starterBundleHelpText": "メインモデル,コントロールネット,IPアダプターなど,ベースモデルから始めるのに必要なすべてのモデルを簡単にインストールできます.バンドルを選択すると,すでにインストールされているモデルはスキップされます.", "inplaceInstallDesc": "ファイルを移動せずにモデルをインストールします.このモデルを使ったとき、元の場所からロードされます.利用できない場合、モデルファイルはInvoke管理モデルディレクトリにインストールしている間に移動されます。", - "hfTokenUnableToVerifyErrorMessage": "ハギングフェイストークンを確認できません.ネットワークによるエラーの可能性があります.後ほどトライしてください.", + "hfTokenUnableToVerifyErrorMessage": "HuggingFaceトークンを確認できません。ネットワークによるエラーの可能性があります。後ほどトライしてください。", "restoreDefaultSettings": "クリックするとモデルのデフォルト設定が使用されます.", - "hfTokenSaved": "ハギングフェイストークンを保存しました", + "hfTokenSaved": "HuggingFaceトークンを保存しました", "imageEncoderModelId": "画像エンコーダーモデルID", "includesNModels": "{{n}}個のモデルとこれらの依存関係を含みます。", "learnMoreAboutSupportedModels": "私たちのサポートしているモデルについて更に学ぶ", @@ -711,7 +711,7 @@ "modelPickerFallbackNoModelsInstalled2": "モデルマネージャー にアクセスしてモデルをインストールしてください.", "modelPickerFallbackNoModelsInstalled": "モデルがインストールされていません.", "manageModels": "モデル管理", - "hfTokenReset": "ハギングフェイストークンリセット", + "hfTokenReset": "HuggingFaceトークンをリセット", "relatedModels": "関連のあるモデル", "installedModelsCount": "{{total}} モデルのうち {{installed}} 個がインストールされています。", "allNModelsInstalled": "{{count}} 個のモデルがすべてインストールされています", @@ -719,7 +719,7 @@ "nAlreadyInstalled": "{{count}} 個すでにインストールされています", "bundleAlreadyInstalled": "バンドルがすでにインストールされています", "bundleAlreadyInstalledDesc": "{{bundleName}} バンドル内のすべてのモデルはすでにインストールされています。", - "launchpadTab": "ランチパッド", + "launchpadTab": "ローンチパッド", "launchpad": { "welcome": "モデルマネジメントへようこそ", "description": "Invoke プラットフォームのほとんどの機能を利用するには、モデルのインストールが必要です。手動インストールオプションから選択するか、厳選されたスターターモデルをご覧ください。", @@ -742,7 +742,10 @@ "installBundleMsg2": "このバンドルでは、次の {{count}} モデルがインストールされます:", "ipAdapters": "IPアダプター", "showOnlyRelatedModels": "関連している", - "starterModelsInModelManager": "スターターモデルはモデルマネージャーにあります" + "starterModelsInModelManager": "スターターモデルはモデルマネージャーにあります", + "actions": "一括操作", + "selectAll": "全て選択", + "deselectAll": "全て選択解除" }, "parameters": { "images": "画像", @@ -752,7 +755,7 @@ "seed": "シード値", "shuffle": "シャッフル", "strength": "強度", - "upscaling": "アップスケーリング", + "upscaling": "アップスケール", "scale": "スケール", "scaleBeforeProcessing": "処理前のスケール", "scaledWidth": "幅のスケール", @@ -794,10 +797,10 @@ "systemDisconnected": "システムが切断されました", "canvasIsTransforming": "キャンバスがビジー状態(変換)", "canvasIsRasterizing": "キャンバスがビジー状態(ラスタライズ)", - "modelIncompatibleBboxHeight": "Bboxの高さは{{height}}ですが,{{model}}は{{multiple}}の倍数が必要です", - "modelIncompatibleScaledBboxHeight": "bboxの高さは{{height}}ですが,{{model}}は{{multiple}}の倍数を必要です", - "modelIncompatibleBboxWidth": "Bboxの幅は{{width}}ですが, {{model}}は{{multiple}}の倍数が必要です", - "modelIncompatibleScaledBboxWidth": "bboxの幅は{{width}}ですが,{{model}}は{{multiple}}の倍数が必要です", + "modelIncompatibleBboxHeight": "バウンディングボックスの高さは{{height}}ですが,{{model}}は{{multiple}}の倍数が必要です", + "modelIncompatibleScaledBboxHeight": "バウンディングボックスの高さは{{height}}ですが,{{model}}は{{multiple}}の倍数を必要です", + "modelIncompatibleBboxWidth": "バウンディングボックスの幅は{{width}}ですが, {{model}}は{{multiple}}の倍数が必要です", + "modelIncompatibleScaledBboxWidth": "バウンディングボックスの幅は{{width}}ですが,{{model}}は{{multiple}}の倍数が必要です", "canvasIsSelectingObject": "キャンバスがビジー状態(オブジェクトの選択)", "noFLUXVAEModelSelected": "FLUX生成にVAEモデルが選択されていません", "noT5EncoderModelSelected": "FLUX生成にT5エンコーダモデルが選択されていません", @@ -806,10 +809,10 @@ "promptExpansionResultPending": "プロンプト拡張結果を受け入れるか破棄してください", "emptyBatches": "空のバッチ", "noStartingFrameImage": "開始フレーム画像がありません", - "fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、bboxの幅は{{width}}です", - "fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、bboxの高さは{{height}}です", - "fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、スケールされたbboxの幅は{{width}}です", - "fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、スケールされた bbox の高さは {{height}} です", + "fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、バウンディングボックスの幅は{{width}}です", + "fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、バウンディングボックスの高さは{{height}}です", + "fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、スケールされたバウンディングボックスの幅は{{width}}です", + "fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、スケールされたバウンディングボックスの高さは {{height}} です", "incompatibleLoRAs": "互換性のない LoRA が追加されました" }, "aspect": "縦横比", @@ -818,7 +821,7 @@ "sendToUpscale": "アップスケーラーに転送", "useSize": "サイズを使用", "postProcessing": "ポストプロセス (Shift + U)", - "denoisingStrength": "ノイズ除去強度", + "denoisingStrength": "除去ノイズ強度", "recallMetadata": "メタデータを再使用", "copyImage": "画像をコピー", "positivePromptPlaceholder": "ポジティブプロンプト", @@ -834,7 +837,7 @@ "imageFit": "初期画像を出力サイズに合わせる", "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (おそらく大きすぎます)", "coherenceEdgeSize": "エッジサイズ", - "swapDimensions": "スワップ次元", + "swapDimensions": "縦横サイズを入れ替え", "controlNetControlMode": "制御モード", "infillColorValue": "塗りつぶし色", "coherenceMinDenoise": "最小ノイズ除去", @@ -845,7 +848,7 @@ "infillMethod": "充填法", "patchmatchDownScaleSize": "ダウンスケール", "boxBlur": "ボックスぼかし", - "remixImage": "リミックス画像", + "remixImage": "画像をリミックス", "processImage": "プロセス画像", "useCpuNoise": "CPUノイズの使用", "staged": "ステージ", @@ -997,8 +1000,8 @@ "noVisibleMasksDesc": "少なくとも1つのインペイントマスクを作成または有効にして反転します", "noInpaintMaskSelected": "インペイントマスクが選択されていません", "noInpaintMaskSelectedDesc": "反転するインペイントマスクを選択", - "invalidBbox": "無効な境界ボックス", - "invalidBboxDesc": "境界ボックスに有効な寸法がありません" + "invalidBbox": "無効なバウンディングボックス", + "invalidBboxDesc": "バウンディングボックスの寸法が有効ではありません" }, "accessibility": { "invokeProgressBar": "進捗バー", @@ -1179,7 +1182,7 @@ "cannotConnectInputToInput": "入力から入力には接続できません", "cannotConnectOutputToOutput": "出力から出力には接続できません", "cannotConnectToSelf": "自身のノードには接続できません", - "colorCodeEdges": "カラーコードエッジ", + "colorCodeEdges": "エッジのカラー化", "loadingNodes": "ノードを読み込み中...", "scheduler": "スケジューラー", "version": "バージョン", @@ -1197,7 +1200,7 @@ "enum": "Enum", "arithmeticSequence": "等差数列", "linearDistribution": "線形分布", - "animatedEdges": "アニメーションエッジ", + "animatedEdges": "エッジのアニメーション", "uniformRandomDistribution": "一様ランダム分布", "noBatchGroup": "グループなし", "parseString": "文字列の解析", @@ -1232,7 +1235,7 @@ "unableToUpdateNode": "ノードアップロード失敗:ノード {{node}} のタイプ {{type}} (削除か再生成が必要かもしれません)", "deletedInvalidEdge": "無効なエッジを削除しました{{source}} -> {{target}}", "collectionFieldType": "{{name}} (コレクション)", - "colorCodeEdgesHelp": "接続されたフィールドによるカラーコードエッジ", + "colorCodeEdgesHelp": "接続されたフィールド種ごとにエッジをカラー化", "showEdgeLabelsHelp": "エッジのラベルを表示,接続されているノードを示す", "sourceNodeFieldDoesNotExist": "無効なエッジ:ソース/アウトプットフィールド{{node}}.{{field}}が存在しません", "deletedMissingNodeFieldFormElement": "不足しているフォームフィールドを削除しました: ノード {{nodeId}} フィールド {{fieldName}}", @@ -1602,13 +1605,13 @@ "compositingMaskAdjustments": { "heading": "マスク調整", "paragraphs": [ - "マスクを調整する." + "マスクを調整する" ] }, "compositingCoherenceMinDenoise": { "paragraphs": [ - "コヒーレンスモードの最小ノイズ除去強度", - "インペインティングまたはアウトペインティング時のコヒーレンス領域の最小ノイズ除去強度" + "コヒーレンスモードの最小除去ノイズ強度", + "インペイント・アウトペイント時のコヒーレンス領域の最小除去ノイズ強度" ], "heading": "最小ノイズ除去" }, @@ -1691,7 +1694,7 @@ "たとえば, プロンプトが 5 つある場合, 各画像は同じシードを使用します.", "「画像ごと」では, 画像ごとに固有のシード値が使用されます. これにより、より多くのバリエーションが得られます." ], - "heading": "シード行動" + "heading": "シードの挙動" }, "imageFit": { "paragraphs": [ @@ -1730,7 +1733,7 @@ "optimizedDenoising": { "heading": "イメージtoイメージの最適化", "paragraphs": [ - "「イメージtoイメージを最適化」を有効にすると、Fluxモデルを用いた画像間変換およびインペインティング変換において、より段階的なノイズ除去強度スケールが適用されます。この設定により、画像に適用される変化量を制御する能力が向上しますが、標準のノイズ除去強度スケールを使用したい場合はオフにすることができます。この設定は現在調整中で、ベータ版です。" + "「イメージtoイメージを最適化」を有効にすると、Fluxモデルを用いた画像間変換およびインペイント変換において、より段階的な除去ノイズ強度スケールが適用されます。この設定により、画像に適用される変化量を制御する能力が向上しますが、標準の除去ノイズ強度スケールを使用したい場合はオフにすることができます。この設定は現在調整中で、ベータ版です。" ] }, "refinerPositiveAestheticScore": { @@ -1756,8 +1759,8 @@ "refinerModel": { "heading": "リファイナーモデル", "paragraphs": [ - "生成プロセスの精製部分で使用されるモデル。", - "世代モデルに似ています。" + "生成プロセスのリファイナー部分で使用されるモデル。", + "生成モデルに似ています。" ] }, "refinerCfgScale": { @@ -1833,7 +1836,7 @@ "tileOverlap": { "heading": "タイルオーバーラップ", "paragraphs": [ - "アップスケーリング時の隣接するタイルの重なり具合を制御します。重なり具合の値を大きくするとタイル間の継ぎ目が見えにくくなりますが、メモリ使用量は増加します。", + "アップスケール時の隣接するタイルの重なり具合を制御します。重なり具合の値を大きくするとタイル間の継ぎ目が見えにくくなりますが、メモリ使用量は増加します。", "デフォルト値の 128 はほとんどの場合に適していますが、特定のニーズやメモリの制約に基づいて調整できます。" ] } @@ -1881,8 +1884,8 @@ "resultTitle": "プロンプト拡張完了", "resultSubtitle": "拡張プロンプトの処理方法を選択します:", "insert": "挿入", - "noPromptHistory": "プロンプト履歴が記録されていません。", - "noMatchingPrompts": "履歴にマッチするプロンプトがありません。", + "noPromptHistory": "プロンプトヒストリーが記録されていません。", + "noMatchingPrompts": "マッチするプロンプトがヒストリーにありません。", "toSwitchBetweenPrompts": "プロンプトを切り替えます。" }, "ui": { @@ -1894,7 +1897,7 @@ "gallery": "ギャラリー", "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", "modelsTab": "$t(ui.tabs.models) $t(common.tab)", - "upscaling": "アップスケーリング", + "upscaling": "アップスケール", "upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)", "generate": "生成" }, @@ -1904,7 +1907,7 @@ "scale": "スケール", "helpText": { "promptAdvice": "アップスケールする際は、媒体とスタイルを説明するプロンプトを使用してください。画像内の具体的なコンテンツの詳細を説明することは避けてください。", - "styleAdvice": "アップスケーリングは、画像の全体的なスタイルに最適です。" + "styleAdvice": "アップスケールは、画像の全体的なスタイルに最適です。" }, "uploadImage": { "title": "アップスケール用の画像をアップロードする", @@ -1957,19 +1960,19 @@ "browseAndLoadWorkflows": "既存のワークフローを参照して読み込む", "addStyleRef": { "title": "スタイル参照を追加する", - "description": "画像を追加して外観を転送します。" + "description": "外観を参照するための画像を追加しましょう。" }, "editImage": { "title": "画像を編集", - "description": "絞り込むために画像を追加します。" + "description": "リファインする画像を追加しましょう。" }, "generateFromText": { "title": "テキストから生成", - "description": "プロンプトを入力して呼び出します。" + "description": "プロンプトを入力して生成しましょう。" }, "useALayoutImage": { "title": "レイアウト画像を使用", - "description": "構成を制御するために画像を追加します。" + "description": "構図を制御するための画像を追加しましょう。" }, "generate": { "canvasCalloutTitle": "画像をさらに細かく制御、編集、反復したいですか?", @@ -1997,13 +2000,13 @@ "canvasGroup": "キャンバス", "saveToGalleryGroup": "ギャラリーに保存", "saveCanvasToGallery": "キャンバスをギャラリーに保存", - "saveBboxToGallery": "Bボックスをギャラリーに保存", + "saveBboxToGallery": "バウンディングボックスをギャラリーに保存", "newControlLayer": "新規コントロールレイヤー", "newRasterLayer": "新規ラスターレイヤー", "newInpaintMask": "新規インペイントマスク", "copyToClipboard": "クリップボードにコピー", "copyCanvasToClipboard": "キャンバスをクリップボードにコピー", - "copyBboxToClipboard": "Bボックスをクリップボードにコピー", + "copyBboxToClipboard": "バウンディングボックスをクリップボードにコピー", "newResizedControlLayer": "新しくサイズ変更されたコントロールレイヤー" }, "regionalGuidance": "領域ガイダンス", @@ -2030,7 +2033,7 @@ "rectangle": "矩形", "move": "移動", "eraser": "消しゴム", - "bbox": "Bbox", + "bbox": "バウンディングボックス", "view": "ビュー" }, "saveCanvasToGallery": "キャンバスをギャラリーに保存", @@ -2064,7 +2067,7 @@ "label": "グリッドにスナップ" }, "preserveMask": { - "label": "マスクされた領域を保持", + "label": "マスクされた領域を保護", "alert": "マスクされた領域の保存" }, "isolatedStagingPreview": "分離されたステージングプレビュー", @@ -2072,10 +2075,10 @@ "isolatedLayerPreview": "分離されたレイヤーのプレビュー", "isolatedLayerPreviewDesc": "フィルタリングや変換などの操作を実行するときに、このレイヤーのみを表示するかどうか。", "invertBrushSizeScrollDirection": "ブラシサイズのスクロール反転", - "pressureSensitivity": "圧力感度", + "pressureSensitivity": "筆圧検知", "saveAllImagesToGallery": { - "label": "ギャラリーに新しい生成を送る", - "alert": "キャンバスを経由せず、ギャラリーに新しい生成を送り込む" + "label": "ギャラリーに新しい生成画像を送る", + "alert": "キャンバスを経由せず、ギャラリーに新しい生成を送る" } }, "filter": { @@ -2093,14 +2096,14 @@ "cancel": "キャンセル", "filters": "フィルター", "filterType": "フィルタータイプ", - "autoProcess": "オートプロセス", + "autoProcess": "自動で実行", "process": "プロセス", - "advanced": "アドバンスド", + "advanced": "詳細設定", "processingLayerWith": "{{type}} フィルターを使用した処理レイヤー。", "forMoreControl": "さらに細かく制御するには、以下の「詳細設定」をクリックしてください。", "canny_edge_detection": { - "label": "キャニーエッジ検出", - "description": "Canny エッジ検出アルゴリズムを使用して、選択したレイヤーからエッジ マップを生成します。", + "label": "エッジ検出(Canny)", + "description": "Canny エッジ検出アルゴリズムを使用して、選択したレイヤーから線画を生成します。", "low_threshold": "低閾値", "high_threshold": "高閾値" }, @@ -2115,8 +2118,8 @@ "scale_factor": "スケール係数" }, "depth_anything_depth_estimation": { - "label": "デプスエニシング", - "description": "デプスエニシングモデルを使用して、選択したレイヤーから深度マップを生成します。", + "label": "深度抽出(Depth Anything)", + "description": "Depth Anthingモデルを使用して、選択したレイヤーから深度マップを生成します。", "model_size": "モデルサイズ", "model_size_small": "スモール", "model_size_small_v2": "スモールv2", @@ -2124,50 +2127,50 @@ "model_size_large": "ラージ" }, "dw_openpose_detection": { - "label": "DW オープンポーズ検出", + "label": "ポーズ検出(DW Openpose)", "description": "DW Openpose モデルを使用して、選択したレイヤー内の人間のポーズを検出します。", "draw_hands": "手を描く", "draw_face": "顔を描く", "draw_body": "体を描く" }, "hed_edge_detection": { - "label": "HEDエッジ検出", - "description": "HED エッジ検出モデルを使用して、選択したレイヤーからエッジ マップを生成します。", + "label": "エッジ検出(HED)", + "description": "HED エッジ検出モデルを使用して、選択したレイヤーから線画を生成します。", "scribble": "落書き" }, "lineart_anime_edge_detection": { - "label": "線画アニメのエッジ検出", - "description": "線画アニメエッジ検出モデルを使用して、選択したレイヤーからエッジ マップを生成します。" + "label": "エッジ検出(Lineart Anime)", + "description": "Lineart Animeエッジ検出モデルを使用して、選択したレイヤーから線画を生成します。" }, "lineart_edge_detection": { - "label": "線画エッジ検出", - "description": "線画エッジ検出モデルを使用して、選択したレイヤーからエッジ マップを生成します。", - "coarse": "粗い" + "label": "エッジ検出(Lineart)", + "description": "Linartエッジ検出モデルを使用して、選択したレイヤーから線画を生成します。", + "coarse": "粗く" }, "mediapipe_face_detection": { - "label": "メディアパイプ顔検出", - "description": "メディアパイプ顔検出モデルを使用して、選択したレイヤー内の顔を検出します。", - "max_faces": "マックスフェイス", + "label": "顔検出(MediaPipe)", + "description": "MediaPipe顔検出モデルを使用して、選択したレイヤー内の顔を検出します。", + "max_faces": "最大顔数", "min_confidence": "最小信頼度" }, "mlsd_detection": { - "label": "線分検出", - "description": "MLSD 線分検出モデルを使用して、選択したレイヤーから線分マップを生成します。", + "label": "直線検出(MLSD)", + "description": "MLSD 線分検出モデルを使用して、選択したレイヤーから直線部分を抽出します。", "score_threshold": "スコア閾値", "distance_threshold": "距離閾値" }, "normal_map": { - "label": "ノーマルマップ", + "label": "ノーマルマップ推定", "description": "選択したレイヤーからノーマルマップを生成します。" }, "pidi_edge_detection": { - "label": "PiDiNetエッジ検出", - "description": "PiDiNet エッジ検出モデルを使用して、選択したレイヤーからエッジ マップを生成します。", + "label": "エッジ検出(PiDiNet)", + "description": "PiDiNet エッジ検出モデルを使用して、選択したレイヤーから線画を生成します。", "scribble": "落書き", "quantize_edges": "エッジを量子化する" }, "img_blur": { - "label": "画像をぼかす", + "label": "ぼかし", "description": "選択したレイヤーをぼかします。", "blur_type": "ぼかしの種類", "blur_radius": "半径", @@ -2175,7 +2178,7 @@ "box_type": "ボックス" }, "img_noise": { - "label": "ノイズ画像", + "label": "ノイズ", "description": "選択したレイヤーにノイズを追加します。", "noise_type": "ノイズの種類", "noise_amount": "総計", @@ -2219,26 +2222,26 @@ "newGlobalReferenceImageError": "グローバル参照イメージの作成中に問題が発生しました", "newRegionalReferenceImageOk": "地域参照画像の作成", "newRegionalReferenceImageError": "地域参照画像の作成中に問題が発生しました", - "newControlLayerOk": "制御レイヤーの作成", + "newControlLayerOk": "作成されたコントロールレイヤー", "newControlLayerError": "制御層の作成中に問題が発生しました", "newRasterLayerOk": "ラスターレイヤーを作成しました", "newRasterLayerError": "ラスターレイヤーの作成中に問題が発生しました", - "pullBboxIntoLayerOk": "Bbox をレイヤーにプル", - "pullBboxIntoLayerError": "BBox をレイヤーにプルする際に問題が発生しました", - "pullBboxIntoReferenceImageOk": "Bbox が ReferenceImage にプルされました", - "pullBboxIntoReferenceImageError": "BBox を ReferenceImage にプルする際に問題が発生しました", + "pullBboxIntoLayerOk": "バウンディングボックスをレイヤーに", + "pullBboxIntoLayerError": "バウンディングボックスをレイヤーにする際に問題が発生しました", + "pullBboxIntoReferenceImageOk": "バウンディングボックスが参照画像にされました", + "pullBboxIntoReferenceImageError": "バウンディングボックスを参照画像にする際に問題が発生しました", "regionIsEmpty": "選択した領域は空です", "mergeVisible": "マージを可視化", "mergeVisibleOk": "マージされたレイヤー", "mergeVisibleError": "レイヤーの結合エラー", "mergingLayers": "レイヤーのマージ", "clearHistory": "履歴をクリア", - "bboxOverlay": "Bboxオーバーレイを表示", + "bboxOverlay": "バウンディングボックスのオーバーレイを表示", "ruleOfThirds": "三分割法を表示", "newSession": "新しいセッション", "clearCaches": "キャッシュをクリア", "recalculateRects": "長方形を再計算する", - "clipToBbox": "ストロークをBboxにクリップ", + "clipToBbox": "ストロークをバウンディングボックス内に制限", "outputOnlyMaskedRegions": "生成された領域のみを出力する", "width": "幅", "autoNegative": "オートネガティブ", @@ -2284,13 +2287,13 @@ "pasteTo": "貼り付け先", "pasteToAssets": "アセット", "pasteToAssetsDesc": "アセットに貼り付け", - "pasteToBbox": "Bボックス", - "pasteToBboxDesc": "新しいレイヤー(Bbox内)", + "pasteToBbox": "バウンディングボックス", + "pasteToBboxDesc": "新しいレイヤー(バウンディングボックス内)", "pasteToCanvas": "キャンバス", "pasteToCanvasDesc": "新しいレイヤー(キャンバス内)", - "transparency": "透明性", - "enableTransparencyEffect": "透明効果を有効にする", - "disableTransparencyEffect": "透明効果を無効にする", + "transparency": "透過表示", + "enableTransparencyEffect": "透過表示を有効にする", + "disableTransparencyEffect": "透過表示を無効にする", "hidingType": "{{type}} を非表示", "showingType": "{{type}}を表示", "showNonRasterLayers": "非ラスターレイヤーを表示 (Shift+H)", @@ -2301,24 +2304,24 @@ "unlocked": "ロック解除", "deleteSelected": "選択項目を削除", "replaceLayer": "レイヤーの置き換え", - "pullBboxIntoLayer": "Bboxをレイヤーに引き込む", - "pullBboxIntoReferenceImage": "Bboxを参照画像に取り込む", + "pullBboxIntoLayer": "バウンディングボックスをレイヤーに", + "pullBboxIntoReferenceImage": "バウンディングボックスを参照画像に", "showProgressOnCanvas": "キャンバスに進捗状況を表示", "useImage": "画像を使う", "negativePrompt": "ネガティブプロンプト", "beginEndStepPercentShort": "開始/終了 %", - "resetCanvasLayers": "キャンバスレイヤーをリセット", + "resetCanvasLayers": "キャンバスとレイヤーをリセット", "resetGenerationSettings": "生成設定をリセット", - "controlLayerEmptyState": "画像をアップロード、ギャラリーからこのレイヤーに画像をドラッグ、境界ボックスをこのレイヤーにプル、またはキャンバスに描画して開始します。", - "referenceImageEmptyStateWithCanvasOptions": "開始するには、画像をアップロードするか、ギャラリーからこの参照画像に画像をドラッグするか、境界ボックスをこの参照画像に引き込みます。", + "controlLayerEmptyState": "画像をアップロード、ギャラリーからこのレイヤーに画像をドラッグ、バウンディングボックスをこのレイヤーにする、またはキャンバスに描画して開始します。", + "referenceImageEmptyStateWithCanvasOptions": "開始するには、画像をアップロードするか、ギャラリーからこの参照画像に画像をドラッグするか、バウンディングボックスをこの参照画像にします。", "referenceImageEmptyState": "開始するには、画像をアップロードするか、ギャラリーからこの参照画像に画像をドラッグします。", "imageNoise": "画像ノイズ", "denoiseLimit": "ノイズ除去制限", "warnings": { "problemsFound": "問題が見つかりました", "unsupportedModel": "選択したベースモデルではレイヤーがサポートされていません", - "controlAdapterNoModelSelected": "制御レイヤーモデルが選択されていません", - "controlAdapterIncompatibleBaseModel": "互換性のない制御レイヤーベースモデル", + "controlAdapterNoModelSelected": "コントロールレイヤーのモデルが選択されていません", + "controlAdapterIncompatibleBaseModel": "コントロールレイヤーのベースモデルに互換性がありません", "controlAdapterNoControl": "コントロールが選択/描画されていません", "ipAdapterNoModelSelected": "参照画像モデルが選択されていません", "ipAdapterIncompatibleBaseModel": "互換性のない参照画像ベースモデル", @@ -2329,7 +2332,7 @@ "rgAutoNegativeNotSupported": "選択したベースモデルでは自動否定はサポートされていません", "rgNoRegion": "領域が描画されていません", "fluxFillIncompatibleWithControlLoRA": "コントロールLoRAはFLUX Fillと互換性がありません", - "bboxHidden": "境界ボックスは非表示です(Shift+O で切り替えます)" + "bboxHidden": "バウンディングボックスは非表示です(Shift+O で切り替え)" }, "errors": { "unableToFindImage": "画像が見つかりません", @@ -2370,7 +2373,7 @@ }, "selectObject": { "selectObject": "オブジェクトを選択", - "pointType": "ポイントタイプ", + "pointType": "点タイプ", "invertSelection": "選択範囲を反転", "include": "含む", "exclude": "除外", @@ -2384,7 +2387,7 @@ "dragToMove": "ポイントをドラッグして移動します", "clickToRemove": "ポイントをクリックして削除します", "desc": "対象オブジェクトを1つ選択します。選択が完了したら、適用 をクリックして選択範囲外のすべてを削除するか、選択範囲を新しいレイヤーとして保存します。", - "visualModeDesc": "ビジュアル モードでは、ボックスとポイントの入力を使用してオブジェクトを選択します。", + "visualModeDesc": "ビジュアル モードでは、ボックスと点の入力を使用してオブジェクトを選択します。", "visualMode1": "クリック&ドラッグして、選択したいオブジェクトの周囲にボックスを描きます。オブジェクトより少し大きいか小さいボックスを描くと、より良い結果が得られる場合があります。", "visualMode2": "クリックして緑の include ポイントを追加するか、Shift キーを押しながらクリックして赤の exclude ポイントを追加し、モデルに含める内容と除外する内容を指示します。", "visualMode3": "ポイントは、ボックスの選択を絞り込むために使用することも、独立して使用することもできます。", @@ -2392,13 +2395,13 @@ "promptMode1": "選択するオブジェクトの簡単な説明を入力します。", "promptMode2": "複雑な説明や複数のオブジェクトを避け、簡単な言葉を使用してください。", "model": "モデル", - "segmentAnything1": "何でもセグメント1", - "segmentAnything2": "何でもセグメント2", + "segmentAnything1": "Segment Anything 1", + "segmentAnything2": "Segment Anything 2", "prompt": "プロンプト選択" }, "HUD": { - "bbox": "Bボックス", - "scaledBbox": "スケールされたBボックス", + "bbox": "バウンディングボックス", + "scaledBbox": "スケールされたバウンディングボックス", "entityStatus": { "isFiltering": "{{title}} はフィルタリング中です", "isTransforming": "{{title}}は変化しています", @@ -2418,20 +2421,20 @@ "showResultsOn": "結果を表示", "showResultsOff": "結果を隠す" }, - "fitBboxToMasks": "Bboxをマスクにフィットさせる", + "fitBboxToMasks": "バウンディングボックスをマスクにフィットさせる", "addAdjustments": "調整を追加", "removeAdjustments": "調整を削除", "adjustments": { "simple": "シンプル", - "curves": "曲線", + "curves": "カーブ", "heading": "調整", "expand": "調整を拡張", "collapse": "折りたたみ調整", "brightness": "輝度", "contrast": "コントラスト", - "saturation": "飽和", - "temperature": "温度", - "tint": "色合い", + "saturation": "彩度", + "temperature": "色温度", + "tint": "色相", "sharpness": "シャープネス", "finish": "終了", "reset": "リセット", @@ -2475,7 +2478,8 @@ "off": "オフ", "switchOnStart": "開始時", "switchOnFinish": "終了時" - } + }, + "extractRegion": "領域を抽出" }, "stylePresets": { "clearTemplateSelection": "選択したテンプレートをクリア", @@ -2541,18 +2545,18 @@ "missingUpscaleInitialImage": "アップスケール用の初期画像がありません", "missingUpscaleModel": "アップスケールモデルがありません", "missingTileControlNetModel": "有効なタイル コントロールネットモデルがインストールされていません", - "incompatibleBaseModel": "アップスケーリングにサポートされていないメインモデルアーキテクチャです", - "incompatibleBaseModelDesc": "アップスケーリングはSD1.5およびSDXLアーキテクチャモデルでのみサポートされています。アップスケーリングを有効にするには、メインモデルを変更してください。", + "incompatibleBaseModel": "アップスケールにサポートされていないメインモデルアーキテクチャです", + "incompatibleBaseModelDesc": "アップスケールはSD1.5およびSDXLアーキテクチャモデルでのみサポートされています。アップスケールを有効にするには、メインモデルを変更してください。", "tileControl": "タイルコントロール", "tileSize": "タイルサイズ", "tileOverlap": "タイルオーバーラップ" }, "sdxl": { - "denoisingStrength": "ノイズ除去強度", + "denoisingStrength": "除去ノイズ強度", "scheduler": "スケジューラー", "loading": "ロード中...", "steps": "ステップ", - "refiner": "Refiner", + "refiner": "リファイナー", "noModelsAvailable": "利用できるモデルがありません", "cfgScale": "CFGスケール", "posAestheticScore": "ポジティブ美的スコア", @@ -2594,7 +2598,7 @@ "builder": "フォームビルダー", "text": "テキスト", "row": "行", - "multiLine": "マルチライン", + "multiLine": "テキスト(複数行)", "resetAllNodeFields": "すべてのノードフィールドをリセット", "slider": "スライダー", "layout": "レイアウト", @@ -2604,7 +2608,7 @@ "component": "コンポーネント", "textPlaceholder": "空のテキスト", "addOption": "オプションを追加", - "singleLine": "単線", + "singleLine": "テキスト", "numberInput": "数値入力", "column": "列", "container": "コンテナ", @@ -2682,7 +2686,7 @@ "delete": "削除", "loadMore": "もっと読み込む", "saveWorkflowToProject": "ワークフローをプロジェクトに保存", - "created": "作成されました", + "created": "作成順", "workflowEditorMenu": "ワークフローエディターメニュー", "recentlyOpened": "最近開いた", "opened": "オープン", @@ -2736,9 +2740,9 @@ "seedBehaviour": { "label": "シードの挙動", "perPromptLabel": "画像ごとのシード", - "perIterationLabel": "いてレーションごとのシード", + "perIterationLabel": "イテレーションごとのシード", "perPromptDesc": "それぞれの画像に足して別のシードを使う", - "perIterationDesc": "それぞれのいてレーションに別のシードを使う" + "perIterationDesc": "それぞれのイテレーションに別のシードを使う" }, "showDynamicPrompts": "ダイナミックプロンプトを表示する", "dynamicPrompts": "ダイナミックプロンプト", @@ -2758,7 +2762,7 @@ "whatsNewInInvoke": "Invokeの新機能", "items": [ "オブジェクトの選択 v2: ポイントおよびボックス入力またはテキスト プロンプトによるオブジェクト選択が改善されました。", - "ラスター レイヤーの調整: レイヤーの明るさ、コントラスト、彩度、曲線などを簡単に調整できます。" + "ラスター レイヤーの調整: レイヤーの明度、コントラスト、彩度、カーブなどを簡単に調整できます。" ], "readReleaseNotes": "リリースノートを読む", "watchRecentReleaseVideos": "最近のリリースビデオを見る", From 7f2878f691b2f47c58532687f6c38c70cbbfa9c1 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Tue, 24 Mar 2026 09:52:50 -0400 Subject: [PATCH 09/56] Fix(frontend): Make ordering of multiple FLUX.2 reference images deterministic (#8989) --- .../util/graph/generation/buildFLUXGraph.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts index db7cba5961..ba27e5dbf6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts @@ -271,10 +271,7 @@ export const buildFLUXGraph = async (arg: GraphBuilderArg): Promise getGlobalReferenceImageWarnings(entity, model).length === 0); if (validFlux2RefImageConfigs.length > 0) { - const flux2KontextCollect = g.addNode({ - type: 'collect', - id: getPrefixedId('flux2_kontext_collect'), - }); + let prevCollect: Invocation<'collect'> | null = null; for (const { config } of validFlux2RefImageConfigs) { // FLUX.2 uses the same flux_kontext node - it just packages the image const kontextConditioning = g.addNode({ @@ -282,9 +279,18 @@ export const buildFLUXGraph = async (arg: GraphBuilderArg): Promise Date: Wed, 25 Mar 2026 22:00:13 -0400 Subject: [PATCH 10/56] chore: bump version to 6.12.0.post1 (#8990) * (chore) bump version to 6.12.0.post1 --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index 50363b269f..9bf32e7d0b 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "6.12.0" +__version__ = "6.12.0.post1" From 18315db7f0173b50f3886773797ad6ae892bede2 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Sat, 28 Mar 2026 14:50:57 +0100 Subject: [PATCH 11/56] Chore Ruff check & format --- invokeai/app/api/routers/app_info.py | 4 +- .../services/external_generation/__init__.py | 2 +- .../external_generation/providers/gemini.py | 5 +- .../external_generation/providers/openai.py | 5 +- .../image_util/controlnet_processor.py | 90 +++++++++++-------- .../backend/model_manager/starter_models.py | 6 -- .../test_external_generation_service.py | 8 +- tests/conftest.py | 2 +- 8 files changed, 69 insertions(+), 53 deletions(-) diff --git a/invokeai/app/api/routers/app_info.py b/invokeai/app/api/routers/app_info.py index 1d68bcc3b8..ef1932c22c 100644 --- a/invokeai/app/api/routers/app_info.py +++ b/invokeai/app/api/routers/app_info.py @@ -12,12 +12,12 @@ from pydantic import BaseModel, Field from invokeai.app.api.dependencies import ApiDependencies from invokeai.app.services.config.config_default import ( - DefaultInvokeAIAppConfig, EXTERNAL_PROVIDER_CONFIG_FIELDS, + DefaultInvokeAIAppConfig, InvokeAIAppConfig, get_config, - load_external_api_keys, load_and_migrate_config, + load_external_api_keys, ) from invokeai.app.services.external_generation.external_generation_common import ExternalProviderStatus from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus diff --git a/invokeai/app/services/external_generation/__init__.py b/invokeai/app/services/external_generation/__init__.py index 692da64643..b933811d29 100644 --- a/invokeai/app/services/external_generation/__init__.py +++ b/invokeai/app/services/external_generation/__init__.py @@ -3,9 +3,9 @@ from invokeai.app.services.external_generation.external_generation_base import ( ExternalProvider, ) from invokeai.app.services.external_generation.external_generation_common import ( + ExternalGeneratedImage, ExternalGenerationRequest, ExternalGenerationResult, - ExternalGeneratedImage, ExternalProviderStatus, ExternalReferenceImage, ) diff --git a/invokeai/app/services/external_generation/providers/gemini.py b/invokeai/app/services/external_generation/providers/gemini.py index 7f289cced7..70cf6a0965 100644 --- a/invokeai/app/services/external_generation/providers/gemini.py +++ b/invokeai/app/services/external_generation/providers/gemini.py @@ -6,7 +6,10 @@ import uuid import requests from PIL.Image import Image as PILImageType -from invokeai.app.services.external_generation.errors import ExternalProviderRateLimitError, ExternalProviderRequestError +from invokeai.app.services.external_generation.errors import ( + ExternalProviderRateLimitError, + ExternalProviderRequestError, +) from invokeai.app.services.external_generation.external_generation_base import ExternalProvider from invokeai.app.services.external_generation.external_generation_common import ( ExternalGeneratedImage, diff --git a/invokeai/app/services/external_generation/providers/openai.py b/invokeai/app/services/external_generation/providers/openai.py index 5051cf9cf1..033f6cd4a6 100644 --- a/invokeai/app/services/external_generation/providers/openai.py +++ b/invokeai/app/services/external_generation/providers/openai.py @@ -5,7 +5,10 @@ import io import requests from PIL.Image import Image as PILImageType -from invokeai.app.services.external_generation.errors import ExternalProviderRateLimitError, ExternalProviderRequestError +from invokeai.app.services.external_generation.errors import ( + ExternalProviderRateLimitError, + ExternalProviderRequestError, +) from invokeai.app.services.external_generation.external_generation_base import ExternalProvider from invokeai.app.services.external_generation.external_generation_common import ( ExternalGeneratedImage, diff --git a/invokeai/backend/image_util/controlnet_processor.py b/invokeai/backend/image_util/controlnet_processor.py index 87739f69e1..81eed42097 100644 --- a/invokeai/backend/image_util/controlnet_processor.py +++ b/invokeai/backend/image_util/controlnet_processor.py @@ -14,43 +14,61 @@ def _get_processor_invocation_class(processor_type: str): """Get the invocation class for a processor type.""" # Import processor invocation classes on demand processor_class_map = { - "canny_image_processor": lambda: __import__( - "invokeai.app.invocations.canny", fromlist=["CannyEdgeDetectionInvocation"] - ).CannyEdgeDetectionInvocation, - "hed_image_processor": lambda: __import__( - "invokeai.app.invocations.hed", fromlist=["HEDEdgeDetectionInvocation"] - ).HEDEdgeDetectionInvocation, - "mlsd_image_processor": lambda: __import__( - "invokeai.app.invocations.mlsd", fromlist=["MLSDDetectionInvocation"] - ).MLSDDetectionInvocation, - "depth_anything_image_processor": lambda: __import__( - "invokeai.app.invocations.depth_anything", fromlist=["DepthAnythingDepthEstimationInvocation"] - ).DepthAnythingDepthEstimationInvocation, - "normalbae_image_processor": lambda: __import__( - "invokeai.app.invocations.normal_bae", fromlist=["NormalMapInvocation"] - ).NormalMapInvocation, - "pidi_image_processor": lambda: __import__( - "invokeai.app.invocations.pidi", fromlist=["PiDiNetEdgeDetectionInvocation"] - ).PiDiNetEdgeDetectionInvocation, - "lineart_image_processor": lambda: __import__( - "invokeai.app.invocations.lineart", fromlist=["LineartEdgeDetectionInvocation"] - ).LineartEdgeDetectionInvocation, - "lineart_anime_image_processor": lambda: __import__( - "invokeai.app.invocations.lineart_anime", fromlist=["LineartAnimeEdgeDetectionInvocation"] - ).LineartAnimeEdgeDetectionInvocation, - "content_shuffle_image_processor": lambda: __import__( - "invokeai.app.invocations.content_shuffle", fromlist=["ContentShuffleInvocation"] - ).ContentShuffleInvocation, - "dw_openpose_image_processor": lambda: __import__( - "invokeai.app.invocations.dw_openpose", fromlist=["DWOpenposeDetectionInvocation"] - ).DWOpenposeDetectionInvocation, - "mediapipe_face_processor": lambda: __import__( - "invokeai.app.invocations.mediapipe_face", fromlist=["MediaPipeFaceDetectionInvocation"] - ).MediaPipeFaceDetectionInvocation, + "canny_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.canny", fromlist=["CannyEdgeDetectionInvocation"] + ).CannyEdgeDetectionInvocation + ), + "hed_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.hed", fromlist=["HEDEdgeDetectionInvocation"] + ).HEDEdgeDetectionInvocation + ), + "mlsd_image_processor": lambda: ( + __import__("invokeai.app.invocations.mlsd", fromlist=["MLSDDetectionInvocation"]).MLSDDetectionInvocation + ), + "depth_anything_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.depth_anything", fromlist=["DepthAnythingDepthEstimationInvocation"] + ).DepthAnythingDepthEstimationInvocation + ), + "normalbae_image_processor": lambda: ( + __import__("invokeai.app.invocations.normal_bae", fromlist=["NormalMapInvocation"]).NormalMapInvocation + ), + "pidi_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.pidi", fromlist=["PiDiNetEdgeDetectionInvocation"] + ).PiDiNetEdgeDetectionInvocation + ), + "lineart_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.lineart", fromlist=["LineartEdgeDetectionInvocation"] + ).LineartEdgeDetectionInvocation + ), + "lineart_anime_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.lineart_anime", fromlist=["LineartAnimeEdgeDetectionInvocation"] + ).LineartAnimeEdgeDetectionInvocation + ), + "content_shuffle_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.content_shuffle", fromlist=["ContentShuffleInvocation"] + ).ContentShuffleInvocation + ), + "dw_openpose_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.dw_openpose", fromlist=["DWOpenposeDetectionInvocation"] + ).DWOpenposeDetectionInvocation + ), + "mediapipe_face_processor": lambda: ( + __import__( + "invokeai.app.invocations.mediapipe_face", fromlist=["MediaPipeFaceDetectionInvocation"] + ).MediaPipeFaceDetectionInvocation + ), # Note: zoe_depth_image_processor doesn't have a processor invocation implementation - "color_map_image_processor": lambda: __import__( - "invokeai.app.invocations.color_map", fromlist=["ColorMapInvocation"] - ).ColorMapInvocation, + "color_map_image_processor": lambda: ( + __import__("invokeai.app.invocations.color_map", fromlist=["ColorMapInvocation"]).ColorMapInvocation + ), } if processor_type in processor_class_map: diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py index f047dc48e8..7fe50d91fc 100644 --- a/invokeai/backend/model_manager/starter_models.py +++ b/invokeai/backend/model_manager/starter_models.py @@ -939,9 +939,7 @@ gemini_flash_image = StarterModel( format=ModelFormat.ExternalApi, capabilities=ExternalModelCapabilities( modes=["txt2img", "img2img", "inpaint"], - supports_seed=True, - supports_reference_images=True, max_images_per_request=1, allowed_aspect_ratios=[ @@ -981,9 +979,7 @@ gemini_pro_image_preview = StarterModel( format=ModelFormat.ExternalApi, capabilities=ExternalModelCapabilities( modes=["txt2img", "img2img", "inpaint"], - supports_seed=True, - supports_reference_images=True, max_reference_images=14, max_images_per_request=1, @@ -1003,9 +999,7 @@ gemini_3_1_flash_image_preview = StarterModel( format=ModelFormat.ExternalApi, capabilities=ExternalModelCapabilities( modes=["txt2img", "img2img", "inpaint"], - supports_seed=True, - supports_reference_images=True, max_reference_images=14, max_images_per_request=1, diff --git a/tests/app/services/external_generation/test_external_generation_service.py b/tests/app/services/external_generation/test_external_generation_service.py index 927f60b6ee..89c1a6db31 100644 --- a/tests/app/services/external_generation/test_external_generation_service.py +++ b/tests/app/services/external_generation/test_external_generation_service.py @@ -3,19 +3,19 @@ import logging import pytest from PIL import Image +from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.external_generation.errors import ( ExternalProviderCapabilityError, ExternalProviderNotConfiguredError, ExternalProviderNotFoundError, ) +from invokeai.app.services.external_generation.external_generation_base import ExternalProvider from invokeai.app.services.external_generation.external_generation_common import ( ExternalGeneratedImage, ExternalGenerationRequest, ExternalGenerationResult, ExternalReferenceImage, ) -from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.app.services.external_generation.external_generation_base import ExternalProvider from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService from invokeai.backend.model_manager.configs.external_api import ( ExternalApiModelConfig, @@ -215,9 +215,7 @@ def test_generate_validates_allowed_aspect_ratios_with_bucket_sizes() -> None: def test_generate_happy_path() -> None: - model = _build_model( - ExternalModelCapabilities(modes=["txt2img"], supports_seed=True) - ) + model = _build_model(ExternalModelCapabilities(modes=["txt2img"], supports_seed=True)) request = _build_request(model=model, seed=42) result = ExternalGenerationResult(images=[ExternalGeneratedImage(image=_make_image(), seed=42)]) provider = DummyProvider("openai", configured=True, result=result) diff --git a/tests/conftest.py b/tests/conftest.py index bfd9f070df..92f42375ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,8 +16,8 @@ from invokeai.app.services.boards.boards_default import BoardService from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService +from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage from invokeai.app.services.images.images_default import ImageService from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache from invokeai.app.services.invocation_services import InvocationServices From 813a5e2c2ed81235c5747d983c395219c25282e0 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Sat, 28 Mar 2026 14:59:51 +0100 Subject: [PATCH 12/56] Chore typegen --- .../frontend/web/src/services/api/schema.ts | 130 ++++++++---------- 1 file changed, 55 insertions(+), 75 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 67e1c31221..bfba81695a 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -8069,10 +8069,6 @@ export type components = { width?: number | null; /** Height */ height?: number | null; - /** Steps */ - steps?: number | null; - /** Guidance */ - guidance?: number | null; /** Num Images */ num_images?: number | null; }; @@ -8126,12 +8122,6 @@ export type components = { * @default null */ prompt?: string | null; - /** - * Negative Prompt - * @description Negative prompt - * @default null - */ - negative_prompt?: string | null; /** * Seed * @description Seed for random number generation @@ -8157,17 +8147,11 @@ export type components = { */ height?: number; /** - * Steps - * @description Number of steps to run + * Image Size + * @description Image size preset (e.g. 1K, 2K, 4K) * @default null */ - steps?: number | null; - /** - * Guidance - * @description Guidance strength - * @default null - */ - guidance?: number | null; + image_size?: string | null; /** * @description Init image for img2img/inpaint * @default null @@ -8184,18 +8168,6 @@ export type components = { * @default [] */ reference_images?: components["schemas"]["ImageField"][]; - /** - * Reference Image Weights - * @description Reference image weights - * @default null - */ - reference_image_weights?: number[] | null; - /** - * Reference Image Modes - * @description Reference image modes - * @default null - */ - reference_image_modes?: string[] | null; /** * type * @default external_image_generation @@ -8248,6 +8220,8 @@ export type components = { aspect_ratio_sizes?: { [key: string]: components["schemas"]["ExternalImageSize"]; } | null; + /** Resolution Presets */ + resolution_presets?: components["schemas"]["ExternalResolutionPreset"][] | null; /** Max Reference Images */ max_reference_images?: number | null; /** @@ -8265,7 +8239,7 @@ export type components = { * Name * @enum {string} */ - name: "negative_prompt" | "reference_images" | "dimensions" | "seed" | "steps" | "guidance"; + name: "reference_images" | "dimensions" | "seed"; /** Slider Min */ slider_min?: number | null; /** Slider Max */ @@ -8354,6 +8328,28 @@ export type components = { */ message?: string | null; }; + /** ExternalResolutionPreset */ + ExternalResolutionPreset: { + /** + * Label + * @description Display label, e.g. '1:1 (1K)' + */ + label: string; + /** + * Aspect Ratio + * @description Aspect ratio string, e.g. '1:1' + */ + aspect_ratio: string; + /** + * Image Size + * @description Image size preset, e.g. '1K' + */ + image_size: string; + /** Width */ + width: number; + /** Height */ + height: number; + }; /** * Apply LoRA Collection - FLUX * @description Applies a collection of LoRAs to a FLUX transformer. @@ -11043,12 +11039,6 @@ export type components = { * @default null */ prompt?: string | null; - /** - * Negative Prompt - * @description Negative prompt - * @default null - */ - negative_prompt?: string | null; /** * Seed * @description Seed for random number generation @@ -11074,17 +11064,11 @@ export type components = { */ height?: number; /** - * Steps - * @description Number of steps to run + * Image Size + * @description Image size preset (e.g. 1K, 2K, 4K) * @default null */ - steps?: number | null; - /** - * Guidance - * @description Guidance strength - * @default null - */ - guidance?: number | null; + image_size?: string | null; /** * @description Init image for img2img/inpaint * @default null @@ -11102,17 +11086,17 @@ export type components = { */ reference_images?: components["schemas"]["ImageField"][]; /** - * Reference Image Weights - * @description Reference image weights + * Temperature + * @description Sampling temperature * @default null */ - reference_image_weights?: number[] | null; + temperature?: number | null; /** - * Reference Image Modes - * @description Reference image modes + * Thinking Level + * @description Thinking level for image generation * @default null */ - reference_image_modes?: string[] | null; + thinking_level?: ("minimal" | "high") | null; /** * type * @default gemini_image_generation @@ -22220,12 +22204,6 @@ export type components = { * @default null */ prompt?: string | null; - /** - * Negative Prompt - * @description Negative prompt - * @default null - */ - negative_prompt?: string | null; /** * Seed * @description Seed for random number generation @@ -22251,17 +22229,11 @@ export type components = { */ height?: number; /** - * Steps - * @description Number of steps to run + * Image Size + * @description Image size preset (e.g. 1K, 2K, 4K) * @default null */ - steps?: number | null; - /** - * Guidance - * @description Guidance strength - * @default null - */ - guidance?: number | null; + image_size?: string | null; /** * @description Init image for img2img/inpaint * @default null @@ -22279,17 +22251,25 @@ export type components = { */ reference_images?: components["schemas"]["ImageField"][]; /** - * Reference Image Weights - * @description Reference image weights - * @default null + * Quality + * @description Output image quality + * @default auto + * @enum {string} */ - reference_image_weights?: number[] | null; + quality?: "auto" | "high" | "medium" | "low"; /** - * Reference Image Modes - * @description Reference image modes + * Background + * @description Background transparency handling + * @default auto + * @enum {string} + */ + background?: "auto" | "transparent" | "opaque"; + /** + * Input Fidelity + * @description Fidelity to source images (edits only) * @default null */ - reference_image_modes?: string[] | null; + input_fidelity?: ("low" | "high") | null; /** * type * @default openai_image_generation From 6963cd97baf6540d3716236e32c295d19b153dde Mon Sep 17 00:00:00 2001 From: Jonathan <34005131+JPPhoto@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:35:18 -0400 Subject: [PATCH 13/56] Fix SIGINT shutdown during active inference (#8993) --- .../session_processor_default.py | 3 - .../test_session_processor_shutdown.py | 185 ++++++++++++++++++ 2 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 tests/app/services/test_session_processor_shutdown.py diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index bda6ac98e3..7159c19e74 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -133,9 +133,6 @@ class DefaultSessionRunner(SessionRunnerBase): self._on_after_run_node(invocation, queue_item, output) - except KeyboardInterrupt: - # TODO(psyche): This is expected to be caught in the main thread. Do we need to catch this here? - pass except CanceledException: # A CanceledException is raised during the denoising step callback if the cancel event is set. We don't need # to do any handling here, and no error should be set - just pass and the cancellation will be handled diff --git a/tests/app/services/test_session_processor_shutdown.py b/tests/app/services/test_session_processor_shutdown.py new file mode 100644 index 0000000000..7c321510ed --- /dev/null +++ b/tests/app/services/test_session_processor_shutdown.py @@ -0,0 +1,185 @@ +from contextlib import contextmanager +from threading import Event + +import pytest + +from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output +from invokeai.app.services.session_processor.session_processor_default import DefaultSessionRunner +from tests.dangerously_run_function_in_subprocess import dangerously_run_function_in_subprocess + + +@invocation_output("test_interrupt_output") +class InterruptTestOutput(BaseInvocationOutput): + pass + + +@invocation("test_keyboard_interrupt", version="1.0.0") +class KeyboardInterruptInvocation(BaseInvocation): + def invoke(self, context) -> InterruptTestOutput: + raise KeyboardInterrupt + + +class _DummyStats: + @contextmanager + def collect_stats(self, invocation: BaseInvocation, graph_execution_state_id: str): + yield + + +class _DummyEvents: + def emit_invocation_started(self, queue_item, invocation) -> None: + pass + + def emit_invocation_complete(self, invocation, queue_item, output) -> None: + pass + + def emit_invocation_error(self, queue_item, invocation, error_type, error_message, error_traceback) -> None: + pass + + +class _DummyLogger: + def debug(self, msg) -> None: + pass + + def error(self, msg) -> None: + pass + + +class _DummyConfig: + node_cache_size = 0 + + +def _build_runner(monkeypatch: pytest.MonkeyPatch) -> DefaultSessionRunner: + monkeypatch.setattr( + "invokeai.app.services.session_processor.session_processor_default.build_invocation_context", + lambda data, services, is_canceled: None, + ) + + runner = DefaultSessionRunner() + runner.start( + services=type( + "Services", + (), + { + "performance_statistics": _DummyStats(), + "events": _DummyEvents(), + "logger": _DummyLogger(), + "configuration": _DummyConfig(), + }, + )(), + cancel_event=Event(), + ) + return runner + + +def _build_queue_item(invocation: BaseInvocation): + return type( + "QueueItem", + (), + { + "item_id": 1, + "session_id": "test-session", + "session": type("Session", (), {"prepared_source_mapping": {invocation.id: invocation.id}})(), + }, + )() + + +def test_run_node_propagates_keyboard_interrupt(monkeypatch: pytest.MonkeyPatch) -> None: + runner = _build_runner(monkeypatch) + invocation = KeyboardInterruptInvocation(id="node") + queue_item = _build_queue_item(invocation) + + with pytest.raises(KeyboardInterrupt): + runner.run_node(invocation=invocation, queue_item=queue_item) + + +def test_run_node_does_not_swallow_sigint_in_subprocess() -> None: + def test_func(): + import os + import signal + import threading + import time + from contextlib import contextmanager + from threading import Event + + import invokeai.app.services.session_processor.session_processor_default as session_processor_default + from invokeai.app.invocations.baseinvocation import ( + BaseInvocation, + BaseInvocationOutput, + invocation, + invocation_output, + ) + from invokeai.app.services.session_processor.session_processor_default import DefaultSessionRunner + + @invocation_output("test_interrupt_output_subprocess") + class InterruptTestOutput(BaseInvocationOutput): + pass + + @invocation("test_sigint_during_node", version="1.0.0") + class SigIntDuringNodeInvocation(BaseInvocation): + def invoke(self, context) -> InterruptTestOutput: + timer = threading.Thread(target=lambda: (time.sleep(0.1), os.kill(os.getpid(), signal.SIGINT))) + timer.daemon = True + timer.start() + time.sleep(5) + return InterruptTestOutput() + + class DummyStats: + @contextmanager + def collect_stats(self, invocation: BaseInvocation, graph_execution_state_id: str): + yield + + class DummyEvents: + def emit_invocation_started(self, queue_item, invocation) -> None: + pass + + def emit_invocation_complete(self, invocation, queue_item, output) -> None: + pass + + def emit_invocation_error(self, queue_item, invocation, error_type, error_message, error_traceback) -> None: + pass + + class DummyLogger: + def debug(self, msg) -> None: + pass + + def error(self, msg) -> None: + pass + + class DummyConfig: + node_cache_size = 0 + + session_processor_default.build_invocation_context = lambda data, services, is_canceled: None + + runner = DefaultSessionRunner() + runner.start( + services=type( + "Services", + (), + { + "performance_statistics": DummyStats(), + "events": DummyEvents(), + "logger": DummyLogger(), + "configuration": DummyConfig(), + }, + )(), + cancel_event=Event(), + ) + + invocation = SigIntDuringNodeInvocation(id="node") + queue_item = type( + "QueueItem", + (), + { + "item_id": 1, + "session_id": "test-session", + "session": type("Session", (), {"prepared_source_mapping": {invocation.id: invocation.id}})(), + }, + )() + + runner.run_node(invocation=invocation, queue_item=queue_item) + print("swallowed") + + stdout, stderr, returncode = dangerously_run_function_in_subprocess(test_func) + + assert stdout.strip() == "" + assert returncode != 0, stderr From ed268b1cfcba23b78e04207c39f1a49b349eadb8 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 4 Apr 2026 16:00:21 -0400 Subject: [PATCH 14/56] Feature (frontend): Add invisible watermark decoder node. (#8967) * Initial plan * Add invisible watermark decoding node and utility method Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * chore(frontend): typegen --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- invokeai/app/invocations/image.py | 21 +++++++- .../backend/image_util/invisible_watermark.py | 24 ++++++++- .../frontend/web/src/services/api/schema.ts | 51 +++++++++++++++++-- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 1d5ba44b24..d4a1977319 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -21,7 +21,7 @@ from invokeai.app.invocations.fields import ( WithBoard, WithMetadata, ) -from invokeai.app.invocations.primitives import ImageOutput +from invokeai.app.invocations.primitives import ImageOutput, StringOutput from invokeai.app.services.image_records.image_records_common import ImageCategory from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.misc import SEED_MAX @@ -581,6 +581,25 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithBoard): return ImageOutput.build(image_dto) +@invocation( + "decode_watermark", + title="Decode Invisible Watermark", + tags=["image", "watermark"], + category="image", + version="1.0.0", +) +class DecodeInvisibleWatermarkInvocation(BaseInvocation): + """Decode an invisible watermark from an image.""" + + image: ImageField = InputField(description="The image to decode the watermark from") + length: int = InputField(default=8, description="The expected watermark length in bytes") + + def invoke(self, context: InvocationContext) -> StringOutput: + image = context.images.get_pil(self.image.image_name) + watermark = InvisibleWatermark.decode_watermark(image, self.length) + return StringOutput(value=watermark) + + @invocation( "mask_edge", title="Mask Edge", diff --git a/invokeai/backend/image_util/invisible_watermark.py b/invokeai/backend/image_util/invisible_watermark.py index 5b0b2dbb5b..95c483848c 100644 --- a/invokeai/backend/image_util/invisible_watermark.py +++ b/invokeai/backend/image_util/invisible_watermark.py @@ -9,7 +9,7 @@ import numpy as np from PIL import Image import invokeai.backend.util.logging as logger -from invokeai.backend.image_util.imwatermark.vendor import WatermarkEncoder +from invokeai.backend.image_util.imwatermark.vendor import WatermarkDecoder, WatermarkEncoder class InvisibleWatermark: @@ -25,3 +25,25 @@ class InvisibleWatermark: encoder.set_watermark("bytes", watermark_text.encode("utf-8")) bgr_encoded = encoder.encode(bgr, "dwtDct") return Image.fromarray(cv2.cvtColor(bgr_encoded, cv2.COLOR_BGR2RGB)).convert("RGBA") + + @classmethod + def decode_watermark(cls, image: Image.Image, length: int = 8) -> str: + """Attempt to decode an invisible watermark from an image. + + Args: + image: The PIL Image to decode the watermark from. + length: The expected watermark length in bytes. Must match the length used when encoding. + The WatermarkDecoder requires the length in bits; this value is multiplied by 8 internally. + + Returns: + The decoded watermark text, or an empty string if no watermark is detected or decoding fails. + """ + logger.debug("Attempting to decode invisible watermark") + try: + bgr = cv2.cvtColor(np.array(image.convert("RGB")), cv2.COLOR_RGB2BGR) + decoder = WatermarkDecoder("bytes", length * 8) + watermark_bytes = decoder.decode(bgr, "dwtDct") + return watermark_bytes.decode("utf-8", errors="ignore").rstrip("\x00") + except Exception: + logger.debug("Failed to decode invisible watermark") + return "" diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 895bc6f36a..dd8c490d51 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -6984,6 +6984,46 @@ export type components = { */ type: "dw_openpose_detection"; }; + /** + * Decode Invisible Watermark + * @description Decode an invisible watermark from an image. + */ + DecodeInvisibleWatermarkInvocation: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * @description The image to decode the watermark from + * @default null + */ + image?: components["schemas"]["ImageField"] | null; + /** + * Length + * @description The expected watermark length in bytes + * @default 8 + */ + length?: number; + /** + * type + * @default decode_watermark + * @constant + */ + type: "decode_watermark"; + }; /** * DeleteAllExceptCurrentResult * @description Result of deleting all except current @@ -10657,7 +10697,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; + [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; }; /** * Edges @@ -13879,7 +13919,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -13943,7 +13983,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -14001,6 +14041,7 @@ export type components = { crop_image_to_bounding_box: components["schemas"]["ImageOutput"]; crop_latents: components["schemas"]["LatentsOutput"]; cv_inpaint: components["schemas"]["ImageOutput"]; + decode_watermark: components["schemas"]["StringOutput"]; denoise_latents: components["schemas"]["LatentsOutput"]; denoise_latents_meta: components["schemas"]["LatentsMetaOutput"]; depth_anything_depth_estimation: components["schemas"]["ImageOutput"]; @@ -14249,7 +14290,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -14324,7 +14365,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node From 474d85e5e0edad98fbf11f93eb8a8d11c0fdd77a Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Sat, 4 Apr 2026 22:43:57 +0200 Subject: [PATCH 15/56] feat: add bulk reidentify action for models (#8951) (#8952) * feat: add bulk reidentify action for models (#8951) Add a "Reidentify Models" bulk action to the model manager, allowing users to re-probe multiple models at once instead of one by one. - Backend: POST /api/v2/models/i/bulk_reidentify endpoint with partial failure handling (returns succeeded/failed lists) - Frontend: bulk reidentify mutation, confirmation modal with warning about custom settings reset, toast notifications for all outcomes - i18n: new translation keys for bulk reidentify UI strings * fix typgen * Fix bulk reidentify failing for models without trigger_phrases The bulk reidentify endpoint was directly assigning trigger_phrases without checking if the config type supports it, causing an AttributeError for ControlNet models. Added the same hasattr guard used by the individual reidentify endpoint. Also restored the missing path preservation that the individual endpoint has. --- invokeai/app/api/routers/model_manager.py | 74 ++++++++++++++++ invokeai/frontend/web/public/locales/en.json | 9 ++ .../BulkReidentifyModelsModal.tsx | 70 ++++++++++++++++ .../subpanels/ModelManagerPanel/ModelList.tsx | 74 ++++++++++++++++ .../ModelListBulkActions.tsx | 15 +++- .../web/src/services/api/endpoints/models.ts | 19 +++++ .../frontend/web/src/services/api/schema.ts | 84 +++++++++++++++++++ 7 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/BulkReidentifyModelsModal.tsx diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index 14b18aac7a..65b059ecfc 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -516,6 +516,19 @@ class BulkDeleteModelsResponse(BaseModel): failed: List[dict] = Field(description="List of failed deletions with error messages") +class BulkReidentifyModelsRequest(BaseModel): + """Request body for bulk model reidentification.""" + + keys: List[str] = Field(description="List of model keys to reidentify") + + +class BulkReidentifyModelsResponse(BaseModel): + """Response body for bulk model reidentification.""" + + succeeded: List[str] = Field(description="List of successfully reidentified model keys") + failed: List[dict] = Field(description="List of failed reidentifications with error messages") + + @model_manager_router.post( "/i/bulk_delete", operation_id="bulk_delete_models", @@ -557,6 +570,67 @@ async def bulk_delete_models( return BulkDeleteModelsResponse(deleted=deleted, failed=failed) +@model_manager_router.post( + "/i/bulk_reidentify", + operation_id="bulk_reidentify_models", + responses={ + 200: {"description": "Models reidentified (possibly with some failures)"}, + }, + status_code=200, +) +async def bulk_reidentify_models( + current_admin: AdminUserOrDefault, + request: BulkReidentifyModelsRequest = Body(description="List of model keys to reidentify"), +) -> BulkReidentifyModelsResponse: + """ + Reidentify multiple models by re-probing their weights files. + + Returns a list of successfully reidentified keys and failed reidentifications with error messages. + """ + logger = ApiDependencies.invoker.services.logger + store = ApiDependencies.invoker.services.model_manager.store + models_path = ApiDependencies.invoker.services.configuration.models_path + + succeeded = [] + failed = [] + + for key in request.keys: + try: + config = store.get_model(key) + if pathlib.Path(config.path).is_relative_to(models_path): + model_path = pathlib.Path(config.path) + else: + model_path = models_path / config.path + mod = ModelOnDisk(model_path) + result = ModelConfigFactory.from_model_on_disk(mod) + if result.config is None: + raise InvalidModelException("Unable to identify model format") + + # Retain user-editable fields from the original config + result.config.path = config.path + result.config.key = config.key + result.config.name = config.name + result.config.description = config.description + result.config.cover_image = config.cover_image + if hasattr(config, "trigger_phrases") and hasattr(result.config, "trigger_phrases"): + result.config.trigger_phrases = config.trigger_phrases + result.config.source = config.source + result.config.source_type = config.source_type + + store.replace_model(config.key, result.config) + succeeded.append(key) + logger.info(f"Reidentified model: {key}") + except UnknownModelException as e: + logger.error(f"Failed to reidentify model {key}: {str(e)}") + failed.append({"key": key, "error": str(e)}) + except Exception as e: + logger.error(f"Failed to reidentify model {key}: {str(e)}") + failed.append({"key": key, "error": str(e)}) + + logger.info(f"Bulk reidentify completed: {len(succeeded)} succeeded, {len(failed)} failed") + return BulkReidentifyModelsResponse(succeeded=succeeded, failed=failed) + + @model_manager_router.delete( "/i/{key}/image", operation_id="delete_model_image", diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 0c72fc9510..bc8efd21bf 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1012,6 +1012,15 @@ "reidentifySuccess": "Model reidentified successfully", "reidentifyUnknown": "Unable to identify model", "reidentifyError": "Error reidentifying model", + "reidentifyModels": "Reidentify Models", + "reidentifyModelsConfirm": "Are you sure you want to reidentify {{count}} model(s)? This will re-probe their weights files to determine the correct format and settings.", + "reidentifyWarning": "This will reset any custom settings you may have applied to these models.", + "modelsReidentified": "Successfully reidentified {{count}} model(s)", + "modelsReidentifyFailed": "Failed to reidentify models", + "someModelsFailedToReidentify": "{{count}} model(s) could not be reidentified", + "modelsReidentifiedPartial": "Partially completed", + "someModelsReidentified": "{{succeeded}} reidentified, {{failed}} failed", + "modelsReidentifyError": "Error reidentifying models", "updatePath": "Update Path", "updatePathTooltip": "Update the file path for this model if you have moved the model files to a new location.", "updatePathDescription": "Enter the new path to the model file or directory. Use this if you have manually moved the model files on disk.", diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/BulkReidentifyModelsModal.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/BulkReidentifyModelsModal.tsx new file mode 100644 index 0000000000..2d73b81687 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/BulkReidentifyModelsModal.tsx @@ -0,0 +1,70 @@ +import { + AlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Button, + Flex, + Text, +} from '@invoke-ai/ui-library'; +import { memo, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; + +type BulkReidentifyModelsModalProps = { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + modelCount: number; + isReidentifying?: boolean; +}; + +export const BulkReidentifyModelsModal = memo( + ({ isOpen, onClose, onConfirm, modelCount, isReidentifying = false }: BulkReidentifyModelsModalProps) => { + const { t } = useTranslation(); + const cancelRef = useRef(null); + + return ( + + + + + {t('modelManager.reidentifyModels', { + count: modelCount, + defaultValue: 'Reidentify Models', + })} + + + + + + {t('modelManager.reidentifyModelsConfirm', { + count: modelCount, + defaultValue: `Are you sure you want to reidentify ${modelCount} model(s)? This will re-probe their weights files to determine the correct format and settings.`, + })} + + + {t('modelManager.reidentifyWarning', { + defaultValue: 'This will reset any custom settings you may have applied to these models.', + })} + + + + + + + + + + + + ); + } +); + +BulkReidentifyModelsModal.displayName = 'BulkReidentifyModelsModal'; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx index f3be0b4686..efb5c1add2 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx @@ -18,12 +18,14 @@ import { serializeError } from 'serialize-error'; import { modelConfigsAdapterSelectors, useBulkDeleteModelsMutation, + useBulkReidentifyModelsMutation, useGetMissingModelsQuery, useGetModelConfigsQuery, } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; import { BulkDeleteModelsModal } from './BulkDeleteModelsModal'; +import { BulkReidentifyModelsModal } from './BulkReidentifyModelsModal'; import { FetchingModelsLoader } from './FetchingModelsLoader'; import { MissingModelsProvider } from './MissingModelsContext'; import { ModelListWrapper } from './ModelListWrapper'; @@ -31,6 +33,7 @@ import { ModelListWrapper } from './ModelListWrapper'; const log = logger('models'); export const [useBulkDeleteModal] = buildUseDisclosure(false); +export const [useBulkReidentifyModal] = buildUseDisclosure(false); const ModelList = () => { const dispatch = useAppDispatch(); @@ -40,11 +43,14 @@ const ModelList = () => { const { t } = useTranslation(); const toast = useToast(); const { isOpen, close } = useBulkDeleteModal(); + const { isOpen: isReidentifyOpen, close: closeReidentify } = useBulkReidentifyModal(); const [isDeleting, setIsDeleting] = useState(false); + const [isReidentifying, setIsReidentifying] = useState(false); const { data: allModelsData, isLoading: isLoadingAll } = useGetModelConfigsQuery(); const { data: missingModelsData, isLoading: isLoadingMissing } = useGetMissingModelsQuery(); const [bulkDeleteModels] = useBulkDeleteModelsMutation(); + const [bulkReidentifyModels] = useBulkReidentifyModelsMutation(); const data = filteredModelType === 'missing' ? missingModelsData : allModelsData; const isLoading = filteredModelType === 'missing' ? isLoadingMissing : isLoadingAll; @@ -148,6 +154,67 @@ const ModelList = () => { } }, [bulkDeleteModels, selectedModelKeys, dispatch, close, toast, t]); + const handleConfirmBulkReidentify = useCallback(async () => { + setIsReidentifying(true); + try { + const result = await bulkReidentifyModels({ keys: selectedModelKeys }).unwrap(); + + // Clear selection and close modal + dispatch(clearModelSelection()); + dispatch(setSelectedModelKey(null)); + closeReidentify(); + + if (result.failed.length === 0) { + toast({ + id: 'BULK_REIDENTIFY_SUCCESS', + title: t('modelManager.modelsReidentified', { + count: result.succeeded.length, + defaultValue: `Successfully reidentified ${result.succeeded.length} model(s)`, + }), + status: 'success', + }); + } else if (result.succeeded.length === 0) { + toast({ + id: 'BULK_REIDENTIFY_FAILED', + title: t('modelManager.modelsReidentifyFailed', { + defaultValue: 'Failed to reidentify models', + }), + description: t('modelManager.someModelsFailedToReidentify', { + count: result.failed.length, + defaultValue: `${result.failed.length} model(s) could not be reidentified`, + }), + status: 'error', + }); + } else { + toast({ + id: 'BULK_REIDENTIFY_PARTIAL', + title: t('modelManager.modelsReidentifiedPartial', { + defaultValue: 'Partially completed', + }), + description: t('modelManager.someModelsReidentified', { + succeeded: result.succeeded.length, + failed: result.failed.length, + defaultValue: `${result.succeeded.length} reidentified, ${result.failed.length} failed`, + }), + status: 'warning', + }); + } + + log.info(`Bulk reidentify completed: ${result.succeeded.length} succeeded, ${result.failed.length} failed`); + } catch (err) { + log.error({ error: serializeError(err as Error) }, 'Bulk reidentify error'); + toast({ + id: 'BULK_REIDENTIFY_ERROR', + title: t('modelManager.modelsReidentifyError', { + defaultValue: 'Error reidentifying models', + }), + status: 'error', + }); + } finally { + setIsReidentifying(false); + } + }, [bulkReidentifyModels, selectedModelKeys, dispatch, closeReidentify, toast, t]); + return ( @@ -173,6 +240,13 @@ const ModelList = () => { modelCount={selectedModelKeys.length} isDeleting={isDeleting} /> + ); }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx index 57e970b48d..87995a0cd2 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx @@ -11,7 +11,7 @@ import { } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { t } from 'i18next'; import { memo, useCallback, useMemo } from 'react'; -import { PiCaretDownBold, PiTrashSimpleBold } from 'react-icons/pi'; +import { PiCaretDownBold, PiSparkleFill, PiTrashSimpleBold } from 'react-icons/pi'; import { modelConfigsAdapterSelectors, useGetMissingModelsQuery, @@ -19,7 +19,7 @@ import { } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; -import { useBulkDeleteModal } from './ModelList'; +import { useBulkDeleteModal, useBulkReidentifyModal } from './ModelList'; const ModelListBulkActionsSx: SystemStyleObject = { alignItems: 'center', @@ -40,11 +40,16 @@ export const ModelListBulkActions = memo(({ sx }: ModelListBulkActionsProps) => const { data: allModelsData } = useGetModelConfigsQuery(); const { data: missingModelsData } = useGetMissingModelsQuery(); const bulkDeleteModal = useBulkDeleteModal(); + const bulkReidentifyModal = useBulkReidentifyModal(); const handleBulkDelete = useCallback(() => { bulkDeleteModal.open(); }, [bulkDeleteModal]); + const handleBulkReidentify = useCallback(() => { + bulkReidentifyModal.open(); + }, [bulkReidentifyModal]); + // Calculate displayed (filtered) model keys const displayedModelKeys = useMemo(() => { // Use missing models data when the filter is 'missing' @@ -125,6 +130,12 @@ export const ModelListBulkActions = memo(({ sx }: ModelListBulkActionsProps) => {t('modelManager.actions')} + } onClick={handleBulkReidentify}> + {t('modelManager.reidentifyModels', { + count: selectionCount, + defaultValue: 'Reidentify Models', + })} + } onClick={handleBulkDelete} color="error.300"> {t('modelManager.deleteModels', { count: selectionCount })} diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index 567d63a100..c3d0decd53 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -52,6 +52,14 @@ type BulkDeleteModelsResponse = { failed: string[]; }; +type BulkReidentifyModelsArg = { + keys: string[]; +}; +type BulkReidentifyModelsResponse = { + succeeded: string[]; + failed: string[]; +}; + type ConvertMainModelResponse = paths['/api/v2/models/convert/{key}']['put']['responses']['200']['content']['application/json']; @@ -431,6 +439,16 @@ export const modelsApi = api.injectEndpoints({ } }, }), + bulkReidentifyModels: build.mutation({ + query: ({ keys }) => { + return { + url: buildModelsUrl('i/bulk_reidentify'), + method: 'POST', + body: { keys }, + }; + }, + invalidatesTags: [{ type: 'ModelConfig', id: LIST_TAG }], + }), getOrphanedModels: build.query({ query: () => ({ url: buildModelsUrl('sync/orphaned'), @@ -475,6 +493,7 @@ export const { useResetHFTokenMutation, useEmptyModelCacheMutation, useReidentifyModelMutation, + useBulkReidentifyModelsMutation, useGetOrphanedModelsQuery, useDeleteOrphanedModelsMutation, } = modelsApi; diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index dd8c490d51..2ccb070c6f 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -521,6 +521,28 @@ export type paths = { patch?: never; trace?: never; }; + "/api/v2/models/i/bulk_reidentify": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Bulk Reidentify Models + * @description Reidentify multiple models by re-probing their weights files. + * + * Returns a list of successfully reidentified keys and failed reidentifications with error messages. + */ + post: operations["bulk_reidentify_models"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v2/models/install": { parameters: { query?: never; @@ -3728,6 +3750,35 @@ export type components = { */ bulk_download_item_name: string; }; + /** + * BulkReidentifyModelsRequest + * @description Request body for bulk model reidentification. + */ + BulkReidentifyModelsRequest: { + /** + * Keys + * @description List of model keys to reidentify + */ + keys: string[]; + }; + /** + * BulkReidentifyModelsResponse + * @description Response body for bulk model reidentification. + */ + BulkReidentifyModelsResponse: { + /** + * Succeeded + * @description List of successfully reidentified model keys + */ + succeeded: string[]; + /** + * Failed + * @description List of failed reidentifications with error messages + */ + failed: { + [key: string]: unknown; + }[]; + }; /** CLIPEmbed_Diffusers_G_Config */ CLIPEmbed_Diffusers_G_Config: { /** @@ -29688,6 +29739,39 @@ export interface operations { }; }; }; + bulk_reidentify_models: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BulkReidentifyModelsRequest"]; + }; + }; + responses: { + /** @description Models reidentified (possibly with some failures) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BulkReidentifyModelsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; list_model_installs: { parameters: { query?: never; From 05f4deb68c6a7856aaf48ae320949081f48b23fd Mon Sep 17 00:00:00 2001 From: Valeri Che <38873282+DustyShoe@users.noreply.github.com> Date: Sun, 5 Apr 2026 02:58:45 +0300 Subject: [PATCH 16/56] Feat(Canvas): Add button to hide preview stage thumbnails (#8963) * Feat(Canvas): Add button to hide preview thumbnails in staging area. * Code clean up. Added tests. * Fix: Removed redundant Icon aliases --- invokeai/frontend/web/public/locales/en.json | 2 + .../StagingArea/StagingAreaItemsList.tsx | 22 +++-- .../StagingArea/StagingAreaToolbar.tsx | 14 ++- ...agingAreaToolbarToggleThumbnailsButton.tsx | 29 ++++++ .../components/StagingArea/context.tsx | 3 + .../components/StagingArea/shared.ts | 1 + .../store/canvasStagingAreaSlice.test.ts | 95 +++++++++++++++++++ .../store/canvasStagingAreaSlice.ts | 19 +++- .../src/features/ui/layouts/StagingArea.tsx | 30 +++++- 9 files changed, 198 insertions(+), 17 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleThumbnailsButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.test.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index bc8efd21bf..285dc0817e 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2900,6 +2900,8 @@ "previous": "Previous", "next": "Next", "saveToGallery": "Save To Gallery", + "hideThumbnails": "Hide Thumbnails", + "showThumbnails": "Show Thumbnails", "showResultsOn": "Showing Results", "showResultsOff": "Hiding Results" }, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaItemsList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaItemsList.tsx index 962ad027cc..b1507b9b48 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaItemsList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaItemsList.tsx @@ -11,15 +11,21 @@ import { Virtuoso } from 'react-virtuoso'; import type { S } from 'services/api/types'; import { useStagingAreaContext } from './context'; -import { getQueueItemElementId } from './shared'; +import { getQueueItemElementId, STAGING_AREA_THUMBNAIL_STRIP_HEIGHT } from './shared'; const log = logger('system'); const virtuosoStyles = { width: '100%', - height: '72px', + height: STAGING_AREA_THUMBNAIL_STRIP_HEIGHT, } satisfies CSSProperties; +const applyViewportStyles = (viewport: HTMLElement) => { + viewport.style.overflowX = `var(--os-viewport-overflow-x)`; + viewport.style.overflowY = `var(--os-viewport-overflow-y)`; + viewport.style.textAlign = 'center'; +}; + /** * Scroll the item at the given index into view if it is not currently visible. */ @@ -88,11 +94,7 @@ const useScrollableStagingArea = (rootRef: RefObject) => { defer: true, events: { initialized(osInstance) { - // force overflow styles - const { viewport } = osInstance.elements(); - viewport.style.overflowX = `var(--os-viewport-overflow-x)`; - viewport.style.overflowY = `var(--os-viewport-overflow-y)`; - viewport.style.textAlign = 'center'; + applyViewportStyles(osInstance.elements().viewport); }, }, options: { @@ -113,6 +115,9 @@ const useScrollableStagingArea = (rootRef: RefObject) => { const { current: root } = rootRef; if (scroller && root) { + // Apply the viewport layout styles before overlayscrollbars initializes to avoid a left-aligned first paint. + applyViewportStyles(scroller); + initialize({ target: root, elements: { @@ -131,7 +136,6 @@ const useScrollableStagingArea = (rootRef: RefObject) => { export const StagingAreaItemsList = memo(() => { const canvasManager = useCanvasManager(); - const ctx = useStagingAreaContext(); const virtuosoRef = useRef(null); const rangeRef = useRef({ startIndex: 0, endIndex: 0 }); @@ -143,7 +147,7 @@ export const StagingAreaItemsList = memo(() => { useEffect(() => { return canvasManager.stagingArea.connectToSession(ctx.$items, ctx.$selectedItem); - }, [canvasManager, ctx.$progressData, ctx.$items, ctx.$selectedItem]); + }, [canvasManager, ctx.$items, ctx.$selectedItem]); useEffect(() => { return ctx.$selectedItemIndex.listen((selectedItemIndex) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index ff73a37fc9..b2f1c3776c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -9,12 +9,18 @@ import { StagingAreaToolbarNextButton } from 'features/controlLayers/components/ import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton'; import { StagingAreaToolbarSaveSelectedToGalleryButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton'; import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton'; +import { StagingAreaToolbarToggleThumbnailsButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarToggleThumbnailsButton'; import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { StagingAreaAutoSwitchButtons } from './StagingAreaAutoSwitchButtons'; -export const StagingAreaToolbar = memo(() => { +type Props = { + areThumbnailsVisible: boolean; + onToggleThumbnails: () => void; +}; + +export const StagingAreaToolbar = memo(({ areThumbnailsVisible, onToggleThumbnails }: Props) => { const ctx = useStagingAreaContext(); useHotkeys('meta+left', ctx.selectFirst, { preventDefault: true }); @@ -22,6 +28,12 @@ export const StagingAreaToolbar = memo(() => { return ( + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleThumbnailsButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleThumbnailsButton.tsx new file mode 100644 index 0000000000..24d5cfbe4e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleThumbnailsButton.tsx @@ -0,0 +1,29 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretLineDownBold, PiCaretLineUpBold } from 'react-icons/pi'; + +type Props = { + areThumbnailsVisible: boolean; + onToggle: () => void; +}; + +export const StagingAreaToolbarToggleThumbnailsButton = memo(({ areThumbnailsVisible, onToggle }: Props) => { + const { t } = useTranslation(); + + const label = areThumbnailsVisible + ? t('controlLayers.stagingArea.hideThumbnails', { defaultValue: 'Hide Thumbnails' }) + : t('controlLayers.stagingArea.showThumbnails', { defaultValue: 'Show Thumbnails' }); + + return ( + : } + onClick={onToggle} + colorScheme="invokeBlue" + /> + ); +}); + +StagingAreaToolbarToggleThumbnailsButton.displayName = 'StagingAreaToolbarToggleThumbnailsButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx index 6b8da8dc4d..4b5e1c438c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx @@ -59,6 +59,9 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi }, onDiscard: ({ item_id, status }) => { store.dispatch(canvasQueueItemDiscarded({ itemId: item_id })); + if (selectQueueItems(store.getState()).length === 0) { + store.dispatch(canvasSessionReset()); + } if (status === 'in_progress' || status === 'pending') { store.dispatch(queueApi.endpoints.cancelQueueItem.initiate({ item_id }, { track: false })); } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts index fe98408df5..fd294e2dcb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts @@ -14,6 +14,7 @@ export const getProgressMessage = (data?: S['InvocationProgressEvent'] | null) = export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))'; export const getQueueItemElementId = (index: number) => `queue-item-preview-${index}`; +export const STAGING_AREA_THUMBNAIL_STRIP_HEIGHT = '72px'; export const getOutputImageName = (item: S['SessionQueueItem']) => { const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) => diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.test.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.test.ts new file mode 100644 index 0000000000..a93ce4e8da --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.test.ts @@ -0,0 +1,95 @@ +import type { Equals } from 'tsafe'; +import { assert } from 'tsafe'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { z } from 'zod'; + +const { getPrefixedIdMock } = vi.hoisted(() => ({ + getPrefixedIdMock: vi.fn((prefix: string) => `${prefix}-generated`), +})); + +vi.mock('features/controlLayers/konva/util', () => ({ + getPrefixedId: getPrefixedIdMock, +})); + +import { + canvasSessionReset, + canvasSessionSliceConfig, + canvasSessionThumbnailsVisibilityToggled, +} from './canvasStagingAreaSlice'; + +describe('canvasStagingAreaSlice', () => { + type InitialState = ReturnType; + type SchemaState = z.infer; + + const { reducer } = canvasSessionSliceConfig.slice; + const migrate = canvasSessionSliceConfig.persistConfig?.migrate; + + beforeEach(() => { + getPrefixedIdMock.mockReset(); + getPrefixedIdMock.mockImplementation((prefix: string) => `${prefix}-generated`); + }); + + it('keeps the initial state aligned with the persisted schema', () => { + assert>(); + }); + + it('toggles thumbnail visibility', () => { + const state = canvasSessionSliceConfig.getInitialState(); + + const hidden = reducer(state, canvasSessionThumbnailsVisibilityToggled()); + const shown = reducer(hidden, canvasSessionThumbnailsVisibilityToggled()); + + expect(hidden.areThumbnailsVisible).toBe(false); + expect(shown.areThumbnailsVisible).toBe(true); + }); + + it('resets thumbnails visibility and discarded items on session reset', () => { + const state = { + _version: 2 as const, + canvasSessionId: 'canvas-existing', + canvasDiscardedQueueItems: [1, 2], + areThumbnailsVisible: false, + }; + + getPrefixedIdMock.mockReturnValueOnce('canvas-reset'); + + const result = reducer(state, canvasSessionReset()); + + expect(result).toEqual({ + _version: 2, + canvasSessionId: 'canvas-reset', + canvasDiscardedQueueItems: [], + areThumbnailsVisible: true, + }); + }); + + it('migrates legacy persisted state without a version to v2 defaults', () => { + expect(migrate).toBeDefined(); + + const result = migrate?.({}); + + expect(result).toEqual({ + _version: 2, + canvasSessionId: 'canvas-generated', + canvasDiscardedQueueItems: [], + areThumbnailsVisible: true, + }); + }); + + it('migrates v1 persisted state while preserving existing session data', () => { + expect(migrate).toBeDefined(); + + const result = migrate?.({ + _version: 1, + canvasSessionId: 'canvas-v1', + canvasDiscardedQueueItems: [3, 5], + }); + + expect(result).toEqual({ + _version: 2, + canvasSessionId: 'canvas-v1', + canvasDiscardedQueueItems: [3, 5], + areThumbnailsVisible: true, + }); + }); +}); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 694abcda1c..9cf2efacd3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -11,16 +11,18 @@ import { assert } from 'tsafe'; import z from 'zod'; const zCanvasStagingAreaState = z.object({ - _version: z.literal(1), + _version: z.literal(2), canvasSessionId: z.string(), canvasDiscardedQueueItems: z.array(z.number().int()), + areThumbnailsVisible: z.boolean(), }); type CanvasStagingAreaState = z.infer; const getInitialState = (): CanvasStagingAreaState => ({ - _version: 1, + _version: 2, canvasSessionId: getPrefixedId('canvas'), canvasDiscardedQueueItems: [], + areThumbnailsVisible: true, }); const slice = createSlice({ @@ -33,11 +35,15 @@ const slice = createSlice({ state.canvasDiscardedQueueItems.push(itemId); } }, + canvasSessionThumbnailsVisibilityToggled: (state) => { + state.areThumbnailsVisible = !state.areThumbnailsVisible; + }, canvasSessionReset: { reducer: (state, action: PayloadAction<{ canvasSessionId: string }>) => { const { canvasSessionId } = action.payload; state.canvasSessionId = canvasSessionId; state.canvasDiscardedQueueItems = []; + state.areThumbnailsVisible = true; }, prepare: () => { return { @@ -50,7 +56,7 @@ const slice = createSlice({ }, }); -export const { canvasSessionReset, canvasQueueItemDiscarded } = slice.actions; +export const { canvasSessionReset, canvasQueueItemDiscarded, canvasSessionThumbnailsVisibilityToggled } = slice.actions; export const canvasSessionSliceConfig: SliceConfig = { slice, @@ -62,6 +68,12 @@ export const canvasSessionSliceConfig: SliceConfig = { if (!('_version' in state)) { state._version = 1; state.canvasSessionId = state.canvasSessionId ?? getPrefixedId('canvas'); + state.canvasDiscardedQueueItems = state.canvasDiscardedQueueItems ?? []; + } + + if (state._version === 1) { + state._version = 2; + state.areThumbnailsVisible = true; } return zCanvasStagingAreaState.parse(state); @@ -71,6 +83,7 @@ export const canvasSessionSliceConfig: SliceConfig = { export const selectCanvasSessionSlice = (s: RootState) => s[slice.name]; export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ canvasSessionId }) => canvasSessionId); +export const selectCanvasSessionAreThumbnailsVisible = (s: RootState) => s[slice.name].areThumbnailsVisible; const selectDiscardedItems = createSelector( selectCanvasSessionSlice, diff --git a/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx b/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx index f262a25daa..552b329872 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx @@ -1,11 +1,23 @@ import { Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { STAGING_AREA_THUMBNAIL_STRIP_HEIGHT } from 'features/controlLayers/components/StagingArea/shared'; import { StagingAreaItemsList } from 'features/controlLayers/components/StagingArea/StagingAreaItemsList'; import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { memo } from 'react'; +import { + canvasSessionThumbnailsVisibilityToggled, + selectCanvasSessionAreThumbnailsVisible, + useCanvasIsStaging, +} from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { memo, useCallback } from 'react'; export const StagingArea = memo(() => { + const dispatch = useAppDispatch(); const isStaging = useCanvasIsStaging(); + const areThumbnailsVisible = useAppSelector(selectCanvasSessionAreThumbnailsVisible); + + const onToggleThumbnails = useCallback(() => { + dispatch(canvasSessionThumbnailsVisibilityToggled()); + }, [dispatch]); if (!isStaging) { return null; @@ -13,8 +25,18 @@ export const StagingArea = memo(() => { return ( - - + + + + ); }); From 5596fa0cc8373859921526a404e10aedc964397c Mon Sep 17 00:00:00 2001 From: Jonathan <34005131+JPPhoto@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:28:15 -0400 Subject: [PATCH 17/56] Upgrade spandrel version (#8996) * Upgrade spandrel to 0.4.2 in uv.lock * Fixed typos --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index a22015f28f..226aecacc3 100644 --- a/uv.lock +++ b/uv.lock @@ -3324,7 +3324,7 @@ wheels = [ [[package]] name = "spandrel" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "einops" }, @@ -3340,9 +3340,9 @@ dependencies = [ { name = "torchvision", version = "0.22.1+rocm6.3", source = { registry = "https://download.pytorch.org/whl/rocm6.3" }, marker = "(extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (extra != 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (extra != 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm')" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/e0/048cd03119a9f2b685a79601a52311d5910ff6fd710c01f4ed6769a2892f/spandrel-0.4.1.tar.gz", hash = "sha256:646d9816a942e59d56aab2dc904353952e57dee4b2cb3f59f7ea4dc0fb11a1f2", size = 233544, upload-time = "2025-01-19T15:31:24.02Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/8f/ab4565c23dd67a036ab72101a830cebd7ca026b2fddf5771bbf6284f6228/spandrel-0.4.2.tar.gz", hash = "sha256:fefa4ea966c6a5b7721dcf24f3e2062a5a96a395c8bedcb570fb55971fdcbccb", size = 247544, upload-time = "2026-02-21T01:52:26.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/1e/5dce7f0d3eb2aa418bd9cf3e84b2f5d2cf45b1c62488dd139fc93c729cfe/spandrel-0.4.1-py3-none-any.whl", hash = "sha256:49a39aa979769749a42203428355bc4840452854d6334ce0d465af46098dd448", size = 305217, upload-time = "2025-01-19T15:31:22.202Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/411ea965835534c43d4b98d451968354876e0e867ea1fd42669e4cca0732/spandrel-0.4.2-py3-none-any.whl", hash = "sha256:6c93e3ecbeb0e548fd2df45a605472b34c1614287c56b51bb33cdef7ae5235b5", size = 320811, upload-time = "2026-02-21T01:52:25.015Z" }, ] [[package]] From 41a542552e3a72f8d43abba3e7b8ed80243cc262 Mon Sep 17 00:00:00 2001 From: Jonathan <34005131+JPPhoto@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:32:35 -0400 Subject: [PATCH 18/56] Fix workflows info copy focus (#9015) * Fix workflow copy hotkeys in info view * Fix Makefile help target copy * Fix workflow info view copy handling * Fix workflow edge delete hotkeys --- Makefile | 8 +- .../web/src/common/hooks/focus.test.ts | 13 ++++ .../features/nodes/components/flow/Flow.tsx | 27 ++++--- .../components/flow/workflowHotkeys.test.ts | 76 +++++++++++++++++++ .../nodes/components/flow/workflowHotkeys.ts | 34 +++++++++ 5 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 invokeai/frontend/web/src/common/hooks/focus.test.ts create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/workflowHotkeys.test.ts create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/workflowHotkeys.ts diff --git a/Makefile b/Makefile index f1e81429e7..2e452c5cc0 100644 --- a/Makefile +++ b/Makefile @@ -12,12 +12,12 @@ help: @echo "mypy-all Run mypy ignoring the config in pyproject.tom but still ignoring missing imports" @echo "test Run the unit tests." @echo "update-config-docstring Update the app's config docstring so mkdocs can autogenerate it correctly." - @echo "frontend-install Install the pnpm modules needed for the front end" - @echo "frontend-build Build the frontend in order to run on localhost:9090" + @echo "frontend-install Install the pnpm modules needed for the frontend" + @echo "frontend-build Build the frontend for localhost:9090" @echo "frontend-dev Run the frontend in developer mode on localhost:5173" @echo "frontend-typegen Generate types for the frontend from the OpenAPI schema" - @echo "frontend-prettier Format the frontend using lint:prettier" - @echo "wheel Build the wheel for the current version" + @echo "frontend-lint Run frontend checks and fixable lint/format steps" + @echo "wheel Build the wheel for the current version" @echo "tag-release Tag the GitHub repository with the current version (use at release time only!)" @echo "openapi Generate the OpenAPI schema for the app, outputting to stdout" @echo "docs Serve the mkdocs site with live reload" diff --git a/invokeai/frontend/web/src/common/hooks/focus.test.ts b/invokeai/frontend/web/src/common/hooks/focus.test.ts new file mode 100644 index 0000000000..c106fe1cec --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/focus.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { getFocusedRegion, setFocusedRegion } from './focus'; + +describe('focus regions', () => { + it('supports the workflows region', () => { + setFocusedRegion('workflows'); + expect(getFocusedRegion()).toBe('workflows'); + + setFocusedRegion(null); + expect(getFocusedRegion()).toBe(null); + }); +}); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index f6474dec74..0c48eddfc6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -51,7 +51,7 @@ import { selectSelectionMode, selectShouldSnapToGrid } from 'features/nodes/stor import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants'; import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; -import type { CSSProperties, MouseEvent } from 'react'; +import type { CSSProperties, MouseEvent, RefObject } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -61,6 +61,7 @@ import InvocationDefaultEdge from './edges/InvocationDefaultEdge'; import CurrentImageNode from './nodes/CurrentImage/CurrentImageNode'; import InvocationNodeWrapper from './nodes/Invocation/InvocationNodeWrapper'; import NotesNode from './nodes/Notes/NotesNode'; +import { isWorkflowHotkeyEnabled, shouldIgnoreWorkflowCopyHotkey } from './workflowHotkeys'; const edgeTypes = { collapsed: InvocationCollapsedEdge, @@ -248,14 +249,14 @@ export const Flow = memo(() => { > - + ); }); Flow.displayName = 'Flow'; -const HotkeyIsolator = memo(() => { +const HotkeyIsolator = memo(({ flowWrapper }: { flowWrapper: RefObject }) => { const mayUndo = useAppSelector(selectMayUndo); const mayRedo = useAppSelector(selectMayRedo); @@ -270,8 +271,12 @@ const HotkeyIsolator = memo(() => { id: 'copySelection', category: 'workflows', callback: copySelection, - options: { enabled: isWorkflowsFocused, preventDefault: true }, - dependencies: [copySelection], + options: { + enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused), + preventDefault: true, + ignoreEventWhen: () => shouldIgnoreWorkflowCopyHotkey(window.getSelection(), flowWrapper.current), + }, + dependencies: [copySelection, isWorkflowsFocused], }); const selectAll = useCallback(() => { @@ -299,7 +304,7 @@ const HotkeyIsolator = memo(() => { id: 'selectAll', category: 'workflows', callback: selectAll, - options: { enabled: isWorkflowsFocused, preventDefault: true }, + options: { enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused), preventDefault: true }, dependencies: [selectAll, isWorkflowsFocused], }); @@ -307,7 +312,7 @@ const HotkeyIsolator = memo(() => { id: 'pasteSelection', category: 'workflows', callback: pasteSelection, - options: { enabled: isWorkflowsFocused, preventDefault: true }, + options: { enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused), preventDefault: true }, dependencies: [pasteSelection, isWorkflowsFocused], }); @@ -315,7 +320,7 @@ const HotkeyIsolator = memo(() => { id: 'pasteSelectionWithEdges', category: 'workflows', callback: pasteSelectionWithEdges, - options: { enabled: isWorkflowsFocused, preventDefault: true }, + options: { enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused), preventDefault: true }, dependencies: [pasteSelectionWithEdges, isWorkflowsFocused], }); @@ -325,7 +330,7 @@ const HotkeyIsolator = memo(() => { callback: () => { store.dispatch(undo()); }, - options: { enabled: isWorkflowsFocused && mayUndo, preventDefault: true }, + options: { enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused) && mayUndo, preventDefault: true }, dependencies: [store, mayUndo, isWorkflowsFocused], }); @@ -335,7 +340,7 @@ const HotkeyIsolator = memo(() => { callback: () => { store.dispatch(redo()); }, - options: { enabled: isWorkflowsFocused && mayRedo, preventDefault: true }, + options: { enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused) && mayRedo, preventDefault: true }, dependencies: [store, mayRedo, isWorkflowsFocused], }); @@ -373,7 +378,7 @@ const HotkeyIsolator = memo(() => { id: 'deleteSelection', category: 'workflows', callback: deleteSelection, - options: { preventDefault: true, enabled: isWorkflowsFocused }, + options: { preventDefault: true, enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused) }, dependencies: [deleteSelection, isWorkflowsFocused], }); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/workflowHotkeys.test.ts b/invokeai/frontend/web/src/features/nodes/components/flow/workflowHotkeys.test.ts new file mode 100644 index 0000000000..e901683d2d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/workflowHotkeys.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest'; + +import { isEventTargetWithinElement, isWorkflowHotkeyEnabled, shouldIgnoreWorkflowCopyHotkey } from './workflowHotkeys'; + +describe('isEventTargetWithinElement', () => { + it('returns true when the element contains the event target', () => { + const target = new EventTarget(); + const element = { + contains: (node: unknown) => node === target, + }; + + expect(isEventTargetWithinElement(target, element as never)).toBe(true); + }); + + it('returns false when the element does not contain the event target', () => { + const target = new EventTarget(); + const element = { + contains: () => false, + }; + + expect(isEventTargetWithinElement(target, element as never)).toBe(false); + }); + + it('returns false when the element is missing', () => { + expect(isEventTargetWithinElement(new EventTarget(), null)).toBe(false); + }); +}); + +describe('isWorkflowHotkeyEnabled', () => { + it('enables workflow hotkeys whenever the workflows pane is focused', () => { + expect(isWorkflowHotkeyEnabled(true)).toBe(true); + }); + + it('disables workflow hotkeys when the workflows pane is not focused', () => { + expect(isWorkflowHotkeyEnabled(false)).toBe(false); + }); +}); + +describe('shouldIgnoreWorkflowCopyHotkey', () => { + const insideNode = new EventTarget() as Node; + const outsideNode = new EventTarget() as Node; + const element = { + contains: (node: Node) => node === insideNode, + }; + + it('returns false when there is no selection', () => { + expect(shouldIgnoreWorkflowCopyHotkey(null, element)).toBe(false); + }); + + it('returns false for collapsed selections', () => { + expect( + shouldIgnoreWorkflowCopyHotkey( + { isCollapsed: true, toString: () => 'text', anchorNode: outsideNode, focusNode: outsideNode }, + element + ) + ).toBe(false); + }); + + it('returns false when the selection is inside the editor element', () => { + expect( + shouldIgnoreWorkflowCopyHotkey( + { isCollapsed: false, toString: () => 'text', anchorNode: insideNode, focusNode: insideNode }, + element + ) + ).toBe(false); + }); + + it('returns true when the selection is outside the editor element', () => { + expect( + shouldIgnoreWorkflowCopyHotkey( + { isCollapsed: false, toString: () => 'text', anchorNode: outsideNode, focusNode: outsideNode }, + element + ) + ).toBe(true); + }); +}); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/workflowHotkeys.ts b/invokeai/frontend/web/src/features/nodes/components/flow/workflowHotkeys.ts new file mode 100644 index 0000000000..4de23face8 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/workflowHotkeys.ts @@ -0,0 +1,34 @@ +export const isEventTargetWithinElement = ( + target: EventTarget | null, + element: { contains: (node: Node) => boolean } | null +) => { + return Boolean(target && element?.contains(target as Node)); +}; + +export const isWorkflowHotkeyEnabled = (isWorkflowsFocused: boolean) => { + return isWorkflowsFocused; +}; + +type SelectionLike = { + isCollapsed: boolean; + toString(): string; + anchorNode: Node | null; + focusNode: Node | null; +}; + +export const shouldIgnoreWorkflowCopyHotkey = ( + selection: SelectionLike | null | undefined, + element: { contains: (node: Node) => boolean } | null +) => { + if (!selection || !element || selection.isCollapsed || selection.toString().length === 0) { + return false; + } + + const nodes = [selection.anchorNode, selection.focusNode].filter((node): node is Node => node !== null); + + if (nodes.length === 0) { + return false; + } + + return nodes.some((node) => !element.contains(node)); +}; From 471ab9d9c0eb1b4eca10f58124aaf0755e66af65 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Sun, 5 Apr 2026 23:59:44 +0200 Subject: [PATCH 19/56] feat: add Inpaint Mask as drag & drop target on canvas (#8942) Closes #8843 Co-authored-by: dunkeroni --- .../components/CanvasDropArea.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx index ebb8e41404..6955b621ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx @@ -12,6 +12,7 @@ const addControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget. const addRegionalGuidanceReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({ type: 'regional_guidance_with_reference_image', }); +const addInpaintMaskFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({ type: 'inpaint_mask' }); const addResizedControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({ type: 'control_layer', withResize: true, @@ -25,7 +26,7 @@ export const CanvasDropArea = memo(() => { <> { left={0} pointerEvents="none" > - + { isDisabled={isBusy} /> - + { isDisabled={isBusy} /> - + { isDisabled={isBusy} /> - + + + + Date: Mon, 6 Apr 2026 01:33:47 +0300 Subject: [PATCH 20/56] Fix to retain layer opacity on mode switch. (#8879) Co-authored-by: dunkeroni --- .../konva/CanvasEntity/CanvasEntityAdapterBase.ts | 1 + .../konva/CanvasEntity/CanvasEntityObjectRenderer.ts | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts index c733334ed0..6751e58da2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts @@ -542,6 +542,7 @@ export abstract class CanvasEntityAdapterBase { - if (!this.parent.konva.layer.visible()) { - return; - } - this.log.trace('Updating opacity'); const opacity = this.parent.state.opacity; From be015a5434852797fc9a00a3f55591eedd8bc48b Mon Sep 17 00:00:00 2001 From: Jonathan <34005131+JPPhoto@users.noreply.github.com> Date: Sun, 5 Apr 2026 19:18:24 -0400 Subject: [PATCH 21/56] Run vitest during frontend build (#9022) * Run vitest during frontend build * Add frontend-test Make target --- Makefile | 5 +++++ invokeai/frontend/web/package.json | 3 ++- invokeai/frontend/web/vite.config.mts | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2e452c5cc0..ecf101f1d5 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ help: @echo "update-config-docstring Update the app's config docstring so mkdocs can autogenerate it correctly." @echo "frontend-install Install the pnpm modules needed for the frontend" @echo "frontend-build Build the frontend for localhost:9090" + @echo "frontend-test Run the frontend test suite once" @echo "frontend-dev Run the frontend in developer mode on localhost:5173" @echo "frontend-typegen Generate types for the frontend from the OpenAPI schema" @echo "frontend-lint Run frontend checks and fixable lint/format steps" @@ -57,6 +58,10 @@ frontend-install: frontend-build: cd invokeai/frontend/web && pnpm build +# Run the frontend test suite once +frontend-test: + cd invokeai/frontend/web && pnpm run test:run + # Run the frontend in dev mode frontend-dev: cd invokeai/frontend/web && pnpm dev diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index da4e31142f..e9a896f1b4 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -21,7 +21,7 @@ "scripts": { "dev": "vite dev", "dev:host": "vite dev --host", - "build": "pnpm run lint && vite build", + "build": "pnpm run lint && vitest run && vite build", "typegen": "node scripts/typegen.js", "preview": "vite preview", "lint:knip": "knip --tags=-knipignore", @@ -35,6 +35,7 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "test": "vitest", + "test:run": "vitest run", "test:ui": "vitest --coverage --ui", "test:no-watch": "vitest --no-watch" }, diff --git a/invokeai/frontend/web/vite.config.mts b/invokeai/frontend/web/vite.config.mts index d15c35d6bc..b3afe5fdeb 100644 --- a/invokeai/frontend/web/vite.config.mts +++ b/invokeai/frontend/web/vite.config.mts @@ -39,6 +39,7 @@ export default defineConfig(({ mode }) => { host: '0.0.0.0', }, test: { + reporters: [['default', { summary: false }]], typecheck: { enabled: true, ignoreSourceErrors: true, From 01c67c546830ccd18715d0d72b7085a969937cd4 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sun, 5 Apr 2026 23:11:44 -0400 Subject: [PATCH 22/56] Fix (multiuser): Ask user to log back in when security token has expired (#9017) * Initial plan * Warn user when credentials have expired in multiuser mode Agent-Logs-Url: https://github.com/lstein/InvokeAI/sessions/f0947cda-b15c-475d-b7f4-2d553bdf2cd6 Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * Address code review: avoid multiple localStorage reads in base query Agent-Logs-Url: https://github.com/lstein/InvokeAI/sessions/f0947cda-b15c-475d-b7f4-2d553bdf2cd6 Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * bugfix(multiuser): ask user to log back in when authentication token expires * feat: sliding window session expiry with token refresh Backend: - SlidingWindowTokenMiddleware refreshes JWT on each mutating request (POST/PUT/PATCH/DELETE), returning a new token in X-Refreshed-Token response header. GET requests don't refresh (they're often background fetches that shouldn't reset the inactivity timer). - CORS expose_headers updated to allow X-Refreshed-Token. Frontend: - dynamicBaseQuery picks up X-Refreshed-Token from responses and updates localStorage so subsequent requests use the fresh expiry. - 401 handler only triggers sessionExpiredLogout when a token was actually sent (not for unauthenticated background requests). - ProtectedRoute polls localStorage every 5s and listens for storage events to detect token removal (e.g. manual deletion, other tabs). Result: session expires after TOKEN_EXPIRATION_NORMAL (1 day) of inactivity, not a fixed time after login. Any user-initiated action resets the clock. Co-Authored-By: Claude Opus 4.6 (1M context) * chore(backend): ruff * fix: address review feedback on auth token handling Bug fixes: - ProtectedRoute: only treat 401 errors as session expiry, not transient 500/network errors that should not force logout - Token refresh: use explicit remember_me claim in JWT instead of inferring from remaining lifetime, preventing silent downgrade of 7-day tokens to 1-day when <24h remains - TokenData: add remember_me field, set during login Tests (6 new): - Mutating requests (POST/PUT/DELETE) return X-Refreshed-Token - GET requests do not return X-Refreshed-Token - Unauthenticated requests do not return X-Refreshed-Token - Remember-me token refreshes to 7-day duration even near expiry - Normal token refreshes to 1-day duration - remember_me claim preserved through refresh cycle Co-Authored-By: Claude Opus 4.6 (1M context) * chore(backend): ruff --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com> --- invokeai/app/api/routers/auth.py | 1 + invokeai/app/api_app.py | 46 +++++ invokeai/app/services/auth/token_service.py | 1 + invokeai/frontend/web/public/locales/en.json | 3 +- .../features/auth/components/LoginPage.tsx | 11 +- .../auth/components/ProtectedRoute.tsx | 37 +++- .../web/src/features/auth/store/authSlice.ts | 18 +- .../frontend/web/src/services/api/index.ts | 42 ++++- tests/app/api/__init__.py | 0 tests/app/api/test_sliding_window_token.py | 168 ++++++++++++++++++ 10 files changed, 309 insertions(+), 18 deletions(-) create mode 100644 tests/app/api/__init__.py create mode 100644 tests/app/api/test_sliding_window_token.py diff --git a/invokeai/app/api/routers/auth.py b/invokeai/app/api/routers/auth.py index b4c1e86cf3..36aeabda82 100644 --- a/invokeai/app/api/routers/auth.py +++ b/invokeai/app/api/routers/auth.py @@ -150,6 +150,7 @@ async def login( user_id=user.user_id, email=user.email, is_admin=user.is_admin, + remember_me=request.remember_me, ) token = create_access_token(token_data, expires_delta) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 49894dba3c..2ca6746b49 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -79,6 +79,50 @@ app = FastAPI( ) +class SlidingWindowTokenMiddleware(BaseHTTPMiddleware): + """Refresh the JWT token on each authenticated response. + + When a request includes a valid Bearer token, the response includes a + X-Refreshed-Token header with a new token that has a fresh expiry. + This implements sliding-window session expiry: the session only expires + after a period of *inactivity*, not a fixed time after login. + """ + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint): + response = await call_next(request) + + # Only refresh on mutating requests (POST/PUT/PATCH/DELETE) — these indicate + # genuine user activity. GET requests are often background fetches (RTK Query + # cache revalidation, refetch-on-focus, etc.) and should not reset the + # inactivity timer. + if response.status_code < 400 and request.method in ("POST", "PUT", "PATCH", "DELETE"): + auth_header = request.headers.get("authorization", "") + if auth_header.startswith("Bearer "): + token = auth_header[7:] + try: + from datetime import timedelta + + from invokeai.app.api.routers.auth import TOKEN_EXPIRATION_NORMAL, TOKEN_EXPIRATION_REMEMBER_ME + from invokeai.app.services.auth.token_service import create_access_token, verify_token + + token_data = verify_token(token) + if token_data is not None: + # Use the remember_me claim from the token to determine the + # correct refresh duration. This avoids the bug where a 7-day + # token with <24h remaining would be silently downgraded to 1 day. + if token_data.remember_me: + expires_delta = timedelta(days=TOKEN_EXPIRATION_REMEMBER_ME) + else: + expires_delta = timedelta(days=TOKEN_EXPIRATION_NORMAL) + + new_token = create_access_token(token_data, expires_delta) + response.headers["X-Refreshed-Token"] = new_token + except Exception: + pass # Don't fail the request if token refresh fails + + return response + + class RedirectRootWithQueryStringMiddleware(BaseHTTPMiddleware): """When a request is made to the root path with a query string, redirect to the root path without the query string. @@ -99,6 +143,7 @@ class RedirectRootWithQueryStringMiddleware(BaseHTTPMiddleware): # Add the middleware app.add_middleware(RedirectRootWithQueryStringMiddleware) +app.add_middleware(SlidingWindowTokenMiddleware) # Add event handler @@ -117,6 +162,7 @@ app.add_middleware( allow_credentials=app_config.allow_credentials, allow_methods=app_config.allow_methods, allow_headers=app_config.allow_headers, + expose_headers=["X-Refreshed-Token"], ) app.add_middleware(GZipMiddleware, minimum_size=1000) diff --git a/invokeai/app/services/auth/token_service.py b/invokeai/app/services/auth/token_service.py index 9c35261c38..2d766bb90a 100644 --- a/invokeai/app/services/auth/token_service.py +++ b/invokeai/app/services/auth/token_service.py @@ -21,6 +21,7 @@ class TokenData(BaseModel): user_id: str email: str is_admin: bool + remember_me: bool = False def set_jwt_secret(secret: str) -> None: diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 285dc0817e..047d5a4007 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -25,7 +25,8 @@ "rememberMe": "Remember me for 7 days", "signIn": "Sign In", "signingIn": "Signing in...", - "loginFailed": "Login failed. Please check your credentials." + "loginFailed": "Login failed. Please check your credentials.", + "sessionExpired": "Your credentials have expired. Please log in again to resume." }, "setup": { "title": "Welcome to InvokeAI", diff --git a/invokeai/frontend/web/src/features/auth/components/LoginPage.tsx b/invokeai/frontend/web/src/features/auth/components/LoginPage.tsx index ddc813163d..b4f01d5878 100644 --- a/invokeai/frontend/web/src/features/auth/components/LoginPage.tsx +++ b/invokeai/frontend/web/src/features/auth/components/LoginPage.tsx @@ -13,8 +13,8 @@ import { Text, VStack, } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { setCredentials } from 'features/auth/store/authSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectSessionExpired, setCredentials } from 'features/auth/store/authSlice'; import type { ChangeEvent, FormEvent } from 'react'; import { memo, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -29,6 +29,7 @@ export const LoginPage = memo(() => { const [rememberMe, setRememberMe] = useState(true); const [login, { isLoading, error }] = useLoginMutation(); const dispatch = useAppDispatch(); + const sessionExpired = useAppSelector(selectSessionExpired); const { data: setupStatus, isLoading: isLoadingSetup } = useGetSetupStatusQuery(); // Redirect to app if multiuser mode is disabled @@ -114,6 +115,12 @@ export const LoginPage = memo(() => { {t('auth.login.title')} + {sessionExpired && ( + + {t('auth.login.sessionExpired')} + + )} + {t('auth.login.email')} { - // If we have a token but fetching user failed, token is invalid - logout - if (userError && isAuthenticated) { - dispatch(logout()); + // Only treat 401 as session expiry. Other errors (500, network, etc.) are + // transient and should not force logout — the 401 handler in dynamicBaseQuery + // already covers the actual expiry case. + if (userError && isAuthenticated && 'status' in userError && userError.status === 401) { + dispatch(sessionExpiredLogout()); navigate('/login', { replace: true }); } }, [userError, isAuthenticated, dispatch, navigate]); + // Detect when auth_token is removed from localStorage (e.g. by another tab, + // browser devtools, or token expiry cleanup). The 'storage' event fires when + // localStorage is modified by another context; we also poll periodically to + // catch same-tab deletions (which don't trigger the storage event). + useEffect(() => { + if (!multiuserEnabled || !isAuthenticated) { + return; + } + + const checkToken = () => { + if (!localStorage.getItem('auth_token') && isAuthenticated) { + dispatch(sessionExpiredLogout()); + navigate('/login', { replace: true }); + } + }; + + // Listen for cross-tab localStorage changes + window.addEventListener('storage', checkToken); + // Poll for same-tab deletions (e.g. browser console) + const interval = setInterval(checkToken, 5000); + + return () => { + window.removeEventListener('storage', checkToken); + clearInterval(interval); + }; + }, [multiuserEnabled, isAuthenticated, dispatch, navigate]); + useEffect(() => { // If we successfully fetched user data, update auth state if (currentUser && token && !user) { diff --git a/invokeai/frontend/web/src/features/auth/store/authSlice.ts b/invokeai/frontend/web/src/features/auth/store/authSlice.ts index 6ac65ef03c..d933c57ed3 100644 --- a/invokeai/frontend/web/src/features/auth/store/authSlice.ts +++ b/invokeai/frontend/web/src/features/auth/store/authSlice.ts @@ -16,6 +16,7 @@ const zAuthState = z.object({ token: z.string().nullable(), user: zUser.nullable(), isLoading: z.boolean(), + sessionExpired: z.boolean(), }); type User = z.infer; @@ -34,6 +35,7 @@ const initialState: AuthState = { token: getStoredAuthToken(), user: null, isLoading: false, + sessionExpired: false, }; const getInitialAuthState = (): AuthState => initialState; @@ -46,6 +48,7 @@ const authSlice = createSlice({ state.token = action.payload.token; state.user = action.payload.user; state.isAuthenticated = true; + state.sessionExpired = false; if (typeof window !== 'undefined' && window.localStorage) { localStorage.setItem('auth_token', action.payload.token); } @@ -54,6 +57,16 @@ const authSlice = createSlice({ state.token = null; state.user = null; state.isAuthenticated = false; + state.sessionExpired = false; + if (typeof window !== 'undefined' && window.localStorage) { + localStorage.removeItem('auth_token'); + } + }, + sessionExpiredLogout: (state) => { + state.token = null; + state.user = null; + state.isAuthenticated = false; + state.sessionExpired = true; if (typeof window !== 'undefined' && window.localStorage) { localStorage.removeItem('auth_token'); } @@ -64,7 +77,7 @@ const authSlice = createSlice({ }, }); -export const { setCredentials, logout, setLoading } = authSlice.actions; +export const { setCredentials, logout, sessionExpiredLogout, setLoading } = authSlice.actions; export const authSliceConfig: SliceConfig = { slice: authSlice, @@ -73,7 +86,7 @@ export const authSliceConfig: SliceConfig = { persistConfig: { migrate: () => getInitialAuthState(), // Don't persist auth state - token is stored in localStorage - persistDenylist: ['isAuthenticated', 'token', 'user', 'isLoading'], + persistDenylist: ['isAuthenticated', 'token', 'user', 'isLoading', 'sessionExpired'], }, }; @@ -81,3 +94,4 @@ export const selectIsAuthenticated = (state: { auth: AuthState }) => state.auth. export const selectCurrentUser = (state: { auth: AuthState }) => state.auth.user; export const selectAuthToken = (state: { auth: AuthState }) => state.auth.token; export const selectIsAuthLoading = (state: { auth: AuthState }) => state.auth.isLoading; +export const selectSessionExpired = (state: { auth: AuthState }) => state.auth.sessionExpired; diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 5be1aa2a67..85a5d320a1 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -7,6 +7,7 @@ import type { TagDescription, } from '@reduxjs/toolkit/query/react'; import { buildCreateApi, coreModule, fetchBaseQuery, reactHooksModule } from '@reduxjs/toolkit/query/react'; +import { sessionExpiredLogout } from 'features/auth/store/authSlice'; import queryString from 'query-string'; import stableHash from 'stable-hash'; @@ -68,22 +69,27 @@ export const getBaseUrl = (): string => { return window.location.origin; }; -const dynamicBaseQuery: BaseQueryFn = (args, api, extraOptions) => { +const dynamicBaseQuery: BaseQueryFn = async ( + args, + api, + extraOptions +) => { const isOpenAPIRequest = (args instanceof Object && args.url.includes('openapi.json')) || (typeof args === 'string' && args.includes('openapi.json')); + const isAuthEndpoint = + (args instanceof Object && + typeof args.url === 'string' && + (args.url.includes('/auth/login') || args.url.includes('/auth/setup'))) || + (typeof args === 'string' && (args.includes('/auth/login') || args.includes('/auth/setup'))); + + const token = localStorage.getItem('auth_token'); + const fetchBaseQueryArgs: FetchBaseQueryArgs = { baseUrl: getBaseUrl(), prepareHeaders: (headers) => { // Add auth token to all requests except setup and login - const token = localStorage.getItem('auth_token'); - const isAuthEndpoint = - (args instanceof Object && - typeof args.url === 'string' && - (args.url.includes('/auth/login') || args.url.includes('/auth/setup'))) || - (typeof args === 'string' && (args.includes('/auth/login') || args.includes('/auth/setup'))); - if (token && !isAuthEndpoint) { headers.set('Authorization', `Bearer ${token}`); } @@ -98,7 +104,25 @@ const dynamicBaseQuery: BaseQueryFn FastAPI: + """Create a minimal FastAPI app with the SlidingWindowTokenMiddleware.""" + from invokeai.app.api_app import SlidingWindowTokenMiddleware + + test_app = FastAPI() + test_app.add_middleware(SlidingWindowTokenMiddleware) + + @test_app.get("/test") + async def get_endpoint(): + return {"ok": True} + + @test_app.post("/test") + async def post_endpoint(): + return {"ok": True} + + @test_app.put("/test") + async def put_endpoint(): + return {"ok": True} + + @test_app.delete("/test") + async def delete_endpoint(): + return {"ok": True} + + return test_app + + +def _make_token(remember_me: bool = False, expires_delta: timedelta | None = None) -> str: + """Create a test token.""" + token_data = TokenData( + user_id="test-user", + email="test@test.com", + is_admin=False, + remember_me=remember_me, + ) + return create_access_token(token_data, expires_delta) + + +class TestSlidingWindowTokenMiddleware: + """Tests for SlidingWindowTokenMiddleware.""" + + def test_mutating_request_returns_refreshed_token(self): + """Authenticated POST/PUT/PATCH/DELETE requests return X-Refreshed-Token.""" + app = _create_test_app() + client = TestClient(app) + token = _make_token() + + for method in ["post", "put", "delete"]: + response = getattr(client, method)("/test", headers={"Authorization": f"Bearer {token}"}) + assert response.status_code == 200 + assert "X-Refreshed-Token" in response.headers, f"{method.upper()} should return refreshed token" + + def test_get_request_does_not_return_refreshed_token(self): + """Authenticated GET requests do NOT return X-Refreshed-Token.""" + app = _create_test_app() + client = TestClient(app) + token = _make_token() + + response = client.get("/test", headers={"Authorization": f"Bearer {token}"}) + assert response.status_code == 200 + assert "X-Refreshed-Token" not in response.headers + + def test_unauthenticated_request_does_not_return_refreshed_token(self): + """Requests without a token do NOT return X-Refreshed-Token.""" + app = _create_test_app() + client = TestClient(app) + + response = client.post("/test") + assert response.status_code == 200 + assert "X-Refreshed-Token" not in response.headers + + def test_remember_me_token_refreshes_to_remember_me_duration(self): + """A remember_me=True token refreshes with the remember-me duration, not the normal duration.""" + from jose import jwt + + from invokeai.app.api.routers.auth import TOKEN_EXPIRATION_REMEMBER_ME + from invokeai.app.services.auth.token_service import ALGORITHM, get_jwt_secret + + app = _create_test_app() + client = TestClient(app) + + # Create a remember-me token with only 1 hour remaining (less than 24h) + token = _make_token(remember_me=True, expires_delta=timedelta(hours=1)) + + response = client.post("/test", headers={"Authorization": f"Bearer {token}"}) + assert "X-Refreshed-Token" in response.headers + + # Decode the refreshed token and check its expiry + refreshed_token = response.headers["X-Refreshed-Token"] + payload = jwt.decode(refreshed_token, get_jwt_secret(), algorithms=[ALGORITHM]) + + # The refreshed token should have ~7 days of remaining life, not ~1 day + from datetime import datetime, timezone + + remaining_seconds = payload["exp"] - datetime.now(timezone.utc).timestamp() + remaining_days = remaining_seconds / 86400 + + # Should be close to TOKEN_EXPIRATION_REMEMBER_ME (7 days), not TOKEN_EXPIRATION_NORMAL (1 day) + assert remaining_days > TOKEN_EXPIRATION_REMEMBER_ME - 0.1, ( + f"Remember-me token was downgraded: {remaining_days:.1f} days remaining, " + f"expected ~{TOKEN_EXPIRATION_REMEMBER_ME}" + ) + + def test_normal_token_refreshes_to_normal_duration(self): + """A remember_me=False token refreshes with the normal duration.""" + from jose import jwt + + from invokeai.app.api.routers.auth import TOKEN_EXPIRATION_NORMAL + from invokeai.app.services.auth.token_service import ALGORITHM, get_jwt_secret + + app = _create_test_app() + client = TestClient(app) + + token = _make_token(remember_me=False) + + response = client.post("/test", headers={"Authorization": f"Bearer {token}"}) + refreshed_token = response.headers["X-Refreshed-Token"] + payload = jwt.decode(refreshed_token, get_jwt_secret(), algorithms=[ALGORITHM]) + + from datetime import datetime, timezone + + remaining_seconds = payload["exp"] - datetime.now(timezone.utc).timestamp() + remaining_days = remaining_seconds / 86400 + + # Should be close to TOKEN_EXPIRATION_NORMAL (1 day), not TOKEN_EXPIRATION_REMEMBER_ME (7 days) + assert remaining_days < TOKEN_EXPIRATION_NORMAL + 0.1, ( + f"Normal token got remember-me duration: {remaining_days:.1f} days" + ) + assert remaining_days > TOKEN_EXPIRATION_NORMAL - 0.1, ( + f"Normal token duration too short: {remaining_days:.1f} days" + ) + + def test_remember_me_claim_preserved_in_refreshed_token(self): + """The remember_me claim is preserved when a token is refreshed.""" + from invokeai.app.services.auth.token_service import verify_token + + app = _create_test_app() + client = TestClient(app) + + # Test with remember_me=True + token = _make_token(remember_me=True) + response = client.post("/test", headers={"Authorization": f"Bearer {token}"}) + refreshed_data = verify_token(response.headers["X-Refreshed-Token"]) + assert refreshed_data is not None + assert refreshed_data.remember_me is True + + # Test with remember_me=False + token = _make_token(remember_me=False) + response = client.post("/test", headers={"Authorization": f"Bearer {token}"}) + refreshed_data = verify_token(response.headers["X-Refreshed-Token"]) + assert refreshed_data is not None + assert refreshed_data.remember_me is False From e6f2980d7c16f41706f40f36caa05b8b10a2d0ec Mon Sep 17 00:00:00 2001 From: Jonathan <34005131+JPPhoto@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:26:26 -0400 Subject: [PATCH 23/56] Added `If` node and ability to link an `Any` output to a node input if cardinality matches (#8869) * Added If node * Added stricter type checking on inputs * feat(nodes): make if-node type checks cardinality-aware without loosening global AnyField * chore: typegen --- invokeai/app/invocations/logic.py | 34 +++ .../store/util/validateConnection.test.ts | 246 ++++++++++++++++++ .../nodes/store/util/validateConnection.ts | 94 ++++++- .../util/validateConnectionTypes.test.ts | 29 +++ .../store/util/validateConnectionTypes.ts | 4 +- .../frontend/web/src/services/api/schema.ts | 77 +++++- tests/test_graph_execution_state.py | 39 +++ 7 files changed, 514 insertions(+), 9 deletions(-) create mode 100644 invokeai/app/invocations/logic.py diff --git a/invokeai/app/invocations/logic.py b/invokeai/app/invocations/logic.py new file mode 100644 index 0000000000..3197427d4e --- /dev/null +++ b/invokeai/app/invocations/logic.py @@ -0,0 +1,34 @@ +from typing import Any, Optional + +from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output +from invokeai.app.invocations.fields import InputField, OutputField, UIType +from invokeai.app.services.shared.invocation_context import InvocationContext + + +@invocation_output("if_output") +class IfInvocationOutput(BaseInvocationOutput): + value: Optional[Any] = OutputField( + default=None, description="The selected value", title="Output", ui_type=UIType.Any + ) + + +@invocation("if", title="If", tags=["logic", "conditional"], category="logic", version="1.0.0") +class IfInvocation(BaseInvocation): + """Selects between two optional inputs based on a boolean condition.""" + + condition: bool = InputField(default=False, description="The condition used to select an input", title="Condition") + true_input: Optional[Any] = InputField( + default=None, + description="Selected when the condition is true", + title="True Input", + ui_type=UIType.Any, + ) + false_input: Optional[Any] = InputField( + default=None, + description="Selected when the condition is false", + title="False Input", + ui_type=UIType.Any, + ) + + def invoke(self, context: InvocationContext) -> IfInvocationOutput: + return IfInvocationOutput(value=self.true_input if self.condition else self.false_input) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts index 947d8745f0..88eae8484f 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.test.ts @@ -1,10 +1,144 @@ import { deepClone } from 'common/util/deepClone'; import { set } from 'es-toolkit/compat'; +import type { InvocationTemplate } from 'features/nodes/types/invocation'; import { describe, expect, it } from 'vitest'; import { add, buildEdge, buildNode, collect, img_resize, main_model_loader, sub, templates } from './testUtils'; import { validateConnection } from './validateConnection'; +const ifTemplate: InvocationTemplate = { + title: 'If', + type: 'if', + version: '1.0.0', + tags: [], + description: 'Selects between two inputs based on a boolean condition', + outputType: 'if_output', + inputs: { + condition: { + name: 'condition', + title: 'Condition', + required: true, + description: 'The condition used to select an input', + fieldKind: 'input', + input: 'connection', + ui_hidden: false, + ui_type: 'BooleanField', + type: { + name: 'BooleanField', + cardinality: 'SINGLE', + batch: false, + }, + default: false, + }, + true_input: { + name: 'true_input', + title: 'True Input', + required: false, + description: 'Selected when condition is true', + fieldKind: 'input', + input: 'connection', + ui_hidden: false, + ui_type: 'AnyField', + type: { + name: 'AnyField', + cardinality: 'SINGLE', + batch: false, + }, + default: undefined, + }, + false_input: { + name: 'false_input', + title: 'False Input', + required: false, + description: 'Selected when condition is false', + fieldKind: 'input', + input: 'connection', + ui_hidden: false, + ui_type: 'AnyField', + type: { + name: 'AnyField', + cardinality: 'SINGLE', + batch: false, + }, + default: undefined, + }, + }, + outputs: { + value: { + fieldKind: 'output', + name: 'value', + title: 'Output', + description: 'The selected value', + type: { + name: 'AnyField', + cardinality: 'SINGLE', + batch: false, + }, + ui_hidden: false, + ui_type: 'AnyField', + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', +}; + +const floatOutputTemplate: InvocationTemplate = { + title: 'Float Output', + type: 'float_output', + version: '1.0.0', + tags: [], + description: 'Outputs a float', + outputType: 'float_output', + inputs: {}, + outputs: { + value: { + fieldKind: 'output', + name: 'value', + title: 'Value', + description: 'Float value', + type: { + name: 'FloatField', + cardinality: 'SINGLE', + batch: false, + }, + ui_hidden: false, + ui_type: 'FloatField', + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', +}; + +const integerCollectionOutputTemplate: InvocationTemplate = { + title: 'Integer Collection Output', + type: 'integer_collection_output', + version: '1.0.0', + tags: [], + description: 'Outputs an integer collection', + outputType: 'integer_collection_output', + inputs: {}, + outputs: { + value: { + fieldKind: 'output', + name: 'value', + title: 'Value', + description: 'Integer collection value', + type: { + name: 'IntegerField', + cardinality: 'COLLECTION', + batch: false, + }, + ui_hidden: false, + ui_type: 'IntegerField', + }, + }, + useCache: true, + nodePack: 'invokeai', + classification: 'stable', +}; + describe(validateConnection.name, () => { it('should reject invalid connection to self', () => { const c = { source: 'add', sourceHandle: 'value', target: 'add', targetHandle: 'a' }; @@ -201,6 +335,118 @@ describe(validateConnection.name, () => { expect(r).toEqual('nodes.fieldTypesMustMatch'); }); + it('should reject mismatched types between if node branch inputs', () => { + const n1 = buildNode(add); + const n2 = buildNode(img_resize); + const n3 = buildNode(ifTemplate); + const nodes = [n1, n2, n3]; + const e1 = buildEdge(n1.id, 'value', n3.id, 'true_input'); + const edges = [e1]; + const c = { source: n2.id, sourceHandle: 'image', target: n3.id, targetHandle: 'false_input' }; + const r = validateConnection(c, nodes, edges, { ...templates, if: ifTemplate }, null); + expect(r).toEqual('nodes.fieldTypesMustMatch'); + }); + + it('should reject mismatched types between if node branch inputs regardless of branch order', () => { + const n1 = buildNode(add); + const n2 = buildNode(img_resize); + const n3 = buildNode(ifTemplate); + const nodes = [n1, n2, n3]; + const e1 = buildEdge(n1.id, 'value', n3.id, 'false_input'); + const edges = [e1]; + const c = { source: n2.id, sourceHandle: 'image', target: n3.id, targetHandle: 'true_input' }; + const r = validateConnection(c, nodes, edges, { ...templates, if: ifTemplate }, null); + expect(r).toEqual('nodes.fieldTypesMustMatch'); + }); + + it('should accept convertible types between if node branch inputs', () => { + const n1 = buildNode(add); + const n2 = buildNode(sub); + const n3 = buildNode(ifTemplate); + const nodes = [n1, n2, n3]; + const e1 = buildEdge(n1.id, 'value', n3.id, 'true_input'); + const edges = [e1]; + const c = { source: n2.id, sourceHandle: 'value', target: n3.id, targetHandle: 'false_input' }; + const r = validateConnection(c, nodes, edges, { ...templates, if: ifTemplate }, null); + expect(r).toEqual(null); + }); + + it('should accept one-way-convertible types between if node branch inputs in either connection order', () => { + const n1 = buildNode(add); + const n2 = buildNode(floatOutputTemplate); + const n3 = buildNode(ifTemplate); + const nodes = [n1, n2, n3]; + const e1 = buildEdge(n1.id, 'value', n3.id, 'false_input'); + const edges = [e1]; + const c = { source: n2.id, sourceHandle: 'value', target: n3.id, targetHandle: 'true_input' }; + const r = validateConnection( + c, + nodes, + edges, + { ...templates, if: ifTemplate, float_output: floatOutputTemplate }, + null + ); + expect(r).toEqual(null); + }); + + it('should accept SINGLE and COLLECTION of the same type between if node branch inputs', () => { + const n1 = buildNode(add); + const n2 = buildNode(integerCollectionOutputTemplate); + const n3 = buildNode(ifTemplate); + const nodes = [n1, n2, n3]; + const e1 = buildEdge(n1.id, 'value', n3.id, 'true_input'); + const edges = [e1]; + const c = { source: n2.id, sourceHandle: 'value', target: n3.id, targetHandle: 'false_input' }; + const r = validateConnection( + c, + nodes, + edges, + { ...templates, if: ifTemplate, integer_collection_output: integerCollectionOutputTemplate }, + null + ); + expect(r).toEqual(null); + }); + + it('should accept if output to collection input when both if branch inputs are collections of matching type', () => { + const n1 = buildNode(integerCollectionOutputTemplate); + const n2 = buildNode(integerCollectionOutputTemplate); + const n3 = buildNode(ifTemplate); + const n4 = buildNode(templates.iterate!); + const nodes = [n1, n2, n3, n4]; + const e1 = buildEdge(n1.id, 'value', n3.id, 'true_input'); + const e2 = buildEdge(n2.id, 'value', n3.id, 'false_input'); + const edges = [e1, e2]; + const c = { source: n3.id, sourceHandle: 'value', target: n4.id, targetHandle: 'collection' }; + const r = validateConnection( + c, + nodes, + edges, + { ...templates, if: ifTemplate, integer_collection_output: integerCollectionOutputTemplate }, + null + ); + expect(r).toEqual(null); + }); + + it('should reject if output to collection input when if branch inputs are not both collection-compatible', () => { + const n1 = buildNode(add); + const n2 = buildNode(integerCollectionOutputTemplate); + const n3 = buildNode(ifTemplate); + const n4 = buildNode(templates.iterate!); + const nodes = [n1, n2, n3, n4]; + const e1 = buildEdge(n1.id, 'value', n3.id, 'true_input'); + const e2 = buildEdge(n2.id, 'value', n3.id, 'false_input'); + const edges = [e1, e2]; + const c = { source: n3.id, sourceHandle: 'value', target: n4.id, targetHandle: 'collection' }; + const r = validateConnection( + c, + nodes, + edges, + { ...templates, if: ifTemplate, integer_collection_output: integerCollectionOutputTemplate }, + null + ); + expect(r).toEqual('nodes.fieldTypesMustMatch'); + }); + it('should reject connections that would create cycles', () => { const n1 = buildNode(add); const n2 = buildNode(sub); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts index 9024a16f42..b342df064b 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts @@ -35,6 +35,23 @@ const getTargetEqualityPredicate = return e.target === c.target && e.targetHandle === c.targetHandle; }; +const IF_INPUT_HANDLES = ['true_input', 'false_input'] as const; + +const isIfInputHandle = (handle: string): handle is (typeof IF_INPUT_HANDLES)[number] => { + return IF_INPUT_HANDLES.includes(handle as (typeof IF_INPUT_HANDLES)[number]); +}; + +const isSingleCollectionPairOfSameBaseType = ( + firstType: { name: string; cardinality: string; batch: boolean }, + secondType: { name: string; cardinality: string; batch: boolean } +) => { + const isSingleToCollection = + firstType.cardinality === 'SINGLE' && secondType.cardinality === 'COLLECTION' && firstType.name === secondType.name; + const isCollectionToSingle = + firstType.cardinality === 'COLLECTION' && secondType.cardinality === 'SINGLE' && firstType.name === secondType.name; + return firstType.batch === secondType.batch && (isSingleToCollection || isCollectionToSingle); +}; + /** * Validates a connection between two fields * @returns A translation key for an error if the connection is invalid, otherwise null @@ -126,6 +143,45 @@ export const validateConnection: ValidateConnectionFunc = ( } } + if (targetNode.data.type === 'if' && isIfInputHandle(c.targetHandle)) { + const siblingHandle = c.targetHandle === 'true_input' ? 'false_input' : 'true_input'; + const siblingInputEdge = filteredEdges.find((e) => e.target === c.target && e.targetHandle === siblingHandle); + + if (siblingInputEdge) { + if (siblingInputEdge.source === null || siblingInputEdge.source === undefined) { + return 'nodes.missingNode'; + } + + if (siblingInputEdge.sourceHandle === null || siblingInputEdge.sourceHandle === undefined) { + return 'nodes.missingFieldTemplate'; + } + + const siblingSourceNode = nodes.find((n) => n.id === siblingInputEdge.source); + if (!siblingSourceNode) { + return 'nodes.missingNode'; + } + + const siblingSourceTemplate = templates[siblingSourceNode.data.type]; + if (!siblingSourceTemplate) { + return 'nodes.missingInvocationTemplate'; + } + + const siblingSourceFieldTemplate = siblingSourceTemplate.outputs[siblingInputEdge.sourceHandle]; + if (!siblingSourceFieldTemplate) { + return 'nodes.missingFieldTemplate'; + } + + const areIfInputTypesCompatible = + validateConnectionTypes(sourceFieldTemplate.type, siblingSourceFieldTemplate.type) || + validateConnectionTypes(siblingSourceFieldTemplate.type, sourceFieldTemplate.type) || + isSingleCollectionPairOfSameBaseType(sourceFieldTemplate.type, siblingSourceFieldTemplate.type); + + if (!areIfInputTypesCompatible) { + return 'nodes.fieldTypesMustMatch'; + } + } + } + if (filteredEdges.find(getTargetEqualityPredicate(c))) { // CollectionItemField inputs can have multiple input connections if (targetFieldTemplate.type.name !== 'CollectionItemField') { @@ -133,7 +189,43 @@ export const validateConnection: ValidateConnectionFunc = ( } } - if (!validateConnectionTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { + if (sourceNode.data.type === 'if' && c.sourceHandle === 'value') { + const ifInputEdges = filteredEdges.filter( + (e) => e.target === sourceNode.id && typeof e.targetHandle === 'string' && isIfInputHandle(e.targetHandle) + ); + const ifInputTypes = ifInputEdges.flatMap((edge) => { + if (edge.source === null || edge.source === undefined) { + return []; + } + if (edge.sourceHandle === null || edge.sourceHandle === undefined) { + return []; + } + const ifInputSourceNode = nodes.find((n) => n.id === edge.source); + if (!ifInputSourceNode) { + return []; + } + const ifInputSourceTemplate = templates[ifInputSourceNode.data.type]; + if (!ifInputSourceTemplate) { + return []; + } + const ifInputSourceFieldTemplate = ifInputSourceTemplate.outputs[edge.sourceHandle]; + if (!ifInputSourceFieldTemplate) { + return []; + } + return [ifInputSourceFieldTemplate.type]; + }); + + if (ifInputTypes.length > 0) { + const areAllIfInputsCompatibleWithTarget = ifInputTypes.every((ifInputType) => + validateConnectionTypes(ifInputType, targetFieldTemplate.type) + ); + if (!areAllIfInputsCompatibleWithTarget) { + return 'nodes.fieldTypesMustMatch'; + } + } else if (!validateConnectionTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { + return 'nodes.fieldTypesMustMatch'; + } + } else if (!validateConnectionTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { return 'nodes.fieldTypesMustMatch'; } } diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts index 755ff4ea38..fc9ce27cb9 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.test.ts @@ -219,6 +219,35 @@ describe(validateConnectionTypes.name, () => { ); expect(r).toBe(true); }); + + it('should accept AnyField source to any SINGLE type', () => { + const r = validateConnectionTypes( + { name: 'AnyField', cardinality: 'SINGLE', batch: false }, + { name: 'StringField', cardinality: 'SINGLE', batch: false } + ); + expect(r).toBe(true); + }); + it('should accept AnyField source to any SINGLE_OR_COLLECTION type', () => { + const r = validateConnectionTypes( + { name: 'AnyField', cardinality: 'SINGLE', batch: false }, + { name: 'StringField', cardinality: 'SINGLE_OR_COLLECTION', batch: false } + ); + expect(r).toBe(true); + }); + it('should reject AnyField SINGLE source to COLLECTION target', () => { + const r = validateConnectionTypes( + { name: 'AnyField', cardinality: 'SINGLE', batch: false }, + { name: 'StringField', cardinality: 'COLLECTION', batch: false } + ); + expect(r).toBe(false); + }); + it('should reject AnyField source when batch mismatches target', () => { + const r = validateConnectionTypes( + { name: 'AnyField', cardinality: 'SINGLE', batch: true }, + { name: 'StringField', cardinality: 'SINGLE', batch: false } + ); + expect(r).toBe(false); + }); }); }); }); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts index 835bf83af0..9ea41435da 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnectionTypes.ts @@ -58,6 +58,7 @@ export const validateConnectionTypes = (sourceType: FieldType, targetType: Field const isSubTypeMatch = doesCardinalityMatch && (isIntToFloat || isIntToString || isFloatToString); const isTargetAnyType = targetType.name === 'AnyField'; + const isSourceAnyType = sourceType.name === 'AnyField' && doesCardinalityMatch; // One of these must be true for the connection to be valid return ( @@ -67,6 +68,7 @@ export const validateConnectionTypes = (sourceType: FieldType, targetType: Field isGenericCollectionToAnyCollectionOrSingleOrCollection || isCollectionToGenericCollection || isSubTypeMatch || - isTargetAnyType + isTargetAnyType || + isSourceAnyType ); }; diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 2ccb070c6f..550a0fa2dd 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -10748,7 +10748,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; + [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; }; /** * Edges @@ -10785,7 +10785,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CogView4ConditioningOutput"] | components["schemas"]["CogView4ModelLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatGeneratorOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["Flux2KleinLoRALoaderOutput"] | components["schemas"]["Flux2KleinModelLoaderOutput"] | components["schemas"]["FluxConditioningCollectionOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxControlLoRALoaderOutput"] | components["schemas"]["FluxControlNetOutput"] | components["schemas"]["FluxFillOutput"] | components["schemas"]["FluxKontextOutput"] | components["schemas"]["FluxLoRALoaderOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["FluxReduxOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageGeneratorOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImagePanelCoordinateOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerGeneratorOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsMetaOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MDControlListOutput"] | components["schemas"]["MDIPAdapterListOutput"] | components["schemas"]["MDT2IAdapterListOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["MetadataToLorasCollectionOutput"] | components["schemas"]["MetadataToModelOutput"] | components["schemas"]["MetadataToSDXLModelOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PBRMapsOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["PromptTemplateOutput"] | components["schemas"]["SD3ConditioningOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["Sd3ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringGeneratorOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"]; + [key: string]: components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CogView4ConditioningOutput"] | components["schemas"]["CogView4ModelLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatGeneratorOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["Flux2KleinLoRALoaderOutput"] | components["schemas"]["Flux2KleinModelLoaderOutput"] | components["schemas"]["FluxConditioningCollectionOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxControlLoRALoaderOutput"] | components["schemas"]["FluxControlNetOutput"] | components["schemas"]["FluxFillOutput"] | components["schemas"]["FluxKontextOutput"] | components["schemas"]["FluxLoRALoaderOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["FluxReduxOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["IfInvocationOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageGeneratorOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImagePanelCoordinateOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerGeneratorOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsMetaOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MDControlListOutput"] | components["schemas"]["MDIPAdapterListOutput"] | components["schemas"]["MDT2IAdapterListOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["MetadataToLorasCollectionOutput"] | components["schemas"]["MetadataToModelOutput"] | components["schemas"]["MetadataToSDXLModelOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PBRMapsOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["PromptTemplateOutput"] | components["schemas"]["SD3ConditioningOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["Sd3ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringGeneratorOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"]; }; /** * Errors @@ -11829,6 +11829,68 @@ export type components = { */ type: "ideal_size_output"; }; + /** + * If + * @description Selects between two optional inputs based on a boolean condition. + */ + IfInvocation: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * Condition + * @description The condition used to select an input + * @default false + */ + condition?: boolean; + /** + * True Input + * @description Selected when the condition is true + * @default null + */ + true_input?: unknown | null; + /** + * False Input + * @description Selected when the condition is false + * @default null + */ + false_input?: unknown | null; + /** + * type + * @default if + * @constant + */ + type: "if"; + }; + /** IfInvocationOutput */ + IfInvocationOutput: { + /** + * Output + * @description The selected value + * @default null + */ + value: unknown | null; + /** + * type + * @default if_output + * @constant + */ + type: "if_output"; + }; /** * Image Batch * @description Create a batched generation, where the workflow is executed once for each image in the batch. @@ -13970,7 +14032,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -13980,7 +14042,7 @@ export type components = { * Result * @description The result of the invocation */ - result: components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CogView4ConditioningOutput"] | components["schemas"]["CogView4ModelLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatGeneratorOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["Flux2KleinLoRALoaderOutput"] | components["schemas"]["Flux2KleinModelLoaderOutput"] | components["schemas"]["FluxConditioningCollectionOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxControlLoRALoaderOutput"] | components["schemas"]["FluxControlNetOutput"] | components["schemas"]["FluxFillOutput"] | components["schemas"]["FluxKontextOutput"] | components["schemas"]["FluxLoRALoaderOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["FluxReduxOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageGeneratorOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImagePanelCoordinateOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerGeneratorOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsMetaOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MDControlListOutput"] | components["schemas"]["MDIPAdapterListOutput"] | components["schemas"]["MDT2IAdapterListOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["MetadataToLorasCollectionOutput"] | components["schemas"]["MetadataToModelOutput"] | components["schemas"]["MetadataToSDXLModelOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PBRMapsOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["PromptTemplateOutput"] | components["schemas"]["SD3ConditioningOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["Sd3ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringGeneratorOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"]; + result: components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CogView4ConditioningOutput"] | components["schemas"]["CogView4ModelLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatGeneratorOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["Flux2KleinLoRALoaderOutput"] | components["schemas"]["Flux2KleinModelLoaderOutput"] | components["schemas"]["FluxConditioningCollectionOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxControlLoRALoaderOutput"] | components["schemas"]["FluxControlNetOutput"] | components["schemas"]["FluxFillOutput"] | components["schemas"]["FluxKontextOutput"] | components["schemas"]["FluxLoRALoaderOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["FluxReduxOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["IfInvocationOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageGeneratorOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImagePanelCoordinateOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerGeneratorOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsMetaOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MDControlListOutput"] | components["schemas"]["MDIPAdapterListOutput"] | components["schemas"]["MDT2IAdapterListOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["MetadataToLorasCollectionOutput"] | components["schemas"]["MetadataToModelOutput"] | components["schemas"]["MetadataToSDXLModelOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PBRMapsOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["PromptTemplateOutput"] | components["schemas"]["SD3ConditioningOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["Sd3ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringGeneratorOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"]; }; /** * InvocationErrorEvent @@ -14034,7 +14096,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -14140,6 +14202,7 @@ export type components = { heuristic_resize: components["schemas"]["ImageOutput"]; i2l: components["schemas"]["LatentsOutput"]; ideal_size: components["schemas"]["IdealSizeOutput"]; + if: components["schemas"]["IfInvocationOutput"]; image: components["schemas"]["ImageOutput"]; image_batch: components["schemas"]["ImageOutput"]; image_collection: components["schemas"]["ImageCollectionOutput"]; @@ -14341,7 +14404,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -14416,7 +14479,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node diff --git a/tests/test_graph_execution_state.py b/tests/test_graph_execution_state.py index e0b8fd4717..39fb88f040 100644 --- a/tests/test_graph_execution_state.py +++ b/tests/test_graph_execution_state.py @@ -5,6 +5,7 @@ import pytest from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext from invokeai.app.invocations.collections import RangeInvocation +from invokeai.app.invocations.logic import IfInvocation, IfInvocationOutput from invokeai.app.invocations.math import AddInvocation, MultiplyInvocation from invokeai.app.services.shared.graph import ( CollectInvocation, @@ -300,6 +301,44 @@ def test_graph_validate_self_collector_without_item_inputs_raises_invalid_edge_e graph.validate_self() +def test_if_invocation_selects_true_input_value(): + invocation = IfInvocation(id="if", condition=True, true_input="true", false_input="false") + + output = invocation.invoke(Mock(InvocationContext)) + + assert output.value == "true" + + +def test_if_invocation_outputs_none_when_selected_input_is_missing(): + invocation = IfInvocation(id="if", condition=False, true_input="true") + + output = invocation.invoke(Mock(InvocationContext)) + + assert output.value is None + + +def test_if_invocation_output_allows_missing_value_on_deserialization(): + output = IfInvocationOutput.model_validate({"type": "if_output"}) + + assert output.value is None + + +def test_if_invocation_output_connects_to_downstream_input(): + graph = Graph() + graph.add_node(IfInvocation(id="if", condition=True, true_input="connected value", false_input="unused")) + graph.add_node(PromptTestInvocation(id="prompt")) + graph.add_edge(create_edge("if", "value", "prompt", "prompt")) + + g = GraphExecutionState(graph=graph) + while not g.is_complete(): + invoke_next(g) + + prepared_prompt_nodes = g.source_prepared_mapping["prompt"] + assert len(prepared_prompt_nodes) == 1 + prepared_prompt_node_id = next(iter(prepared_prompt_nodes)) + assert g.results[prepared_prompt_node_id].prompt == "connected value" + + def test_are_connection_types_compatible_accepts_subclass_to_base(): """A subclass output should be connectable to a base-class input. From 32002bd37e2fd268d546c1fe83e6da456dba5715 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 6 Apr 2026 05:54:25 +0200 Subject: [PATCH 24/56] ui: translations update from weblate (#8992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * translationBot(ui): update translation (Italian) Currently translated at 98.0% (2205 of 2250 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI * translationBot(ui): update translation files Updated by "Remove blank strings" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ Translation: InvokeAI/Web UI * translationBot(ui): update translation (Italian) Currently translated at 97.8% (2210 of 2259 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.8% (2224 of 2272 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 98.1% (2252 of 2295 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 98.0% (2264 of 2309 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Russian) Currently translated at 60.7% (1419 of 2334 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/ * translationBot(ui): update translation (Italian) Currently translated at 98.1% (2290 of 2334 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.7% (2319 of 2372 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.7% (2327 of 2380 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.7% (2328 of 2382 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Italian) Currently translated at 97.5% (2370 of 2429 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Finnish) Currently translated at 1.5% (37 of 2429 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fi/ * translationBot(ui): update translation (Italian) Currently translated at 97.5% (2373 of 2433 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Japanese) Currently translated at 87.1% (2120 of 2433 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/ * translationBot(ui): update translation (Italian) Currently translated at 97.5% (2374 of 2433 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Japanese) Currently translated at 92.2% (2244 of 2433 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/ * translationBot(ui): update translation (Italian) Currently translated at 97.5% (2374 of 2433 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ * translationBot(ui): update translation (Spanish) Currently translated at 29.4% (720 of 2444 strings) Translation: InvokeAI/Web UI Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/es/ --------- Co-authored-by: Riccardo Giovanetti Co-authored-by: DustyShoe Co-authored-by: Ilmari Laakkonen Co-authored-by: 嶋田豪介 Co-authored-by: Lucas Prone --- invokeai/frontend/web/public/locales/es.json | 117 ++++++++++++++- invokeai/frontend/web/public/locales/it.json | 6 +- invokeai/frontend/web/public/locales/ja.json | 148 ++++++++++++++++++- 3 files changed, 257 insertions(+), 14 deletions(-) diff --git a/invokeai/frontend/web/public/locales/es.json b/invokeai/frontend/web/public/locales/es.json index 4c58ea87f5..8f68ea585c 100644 --- a/invokeai/frontend/web/public/locales/es.json +++ b/invokeai/frontend/web/public/locales/es.json @@ -92,7 +92,9 @@ "toResolve": "Para resolver", "outpaint": "outpaint", "simple": "Sencillo", - "close": "Cerrar" + "close": "Cerrar", + "board": "Tablero", + "crop": "Cortar" }, "gallery": { "galleryImageSize": "Tamaño de la imagen", @@ -327,7 +329,7 @@ "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.", + "bottomMessage": "Al eliminarlas imágenes, se restablecerán las funcionalidades que actualmente las estén utilizando.", "deleteBoardAndImages": "Borrar el panel y las imágenes", "loading": "Cargando...", "deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al Seleccionar 'Borrar solo el panel' transferirá las imágenes a un estado sin categorizar.", @@ -354,9 +356,21 @@ "unarchiveBoard": "Desarchivar el panel", "noBoards": "No hay paneles {{boardType}}", "shared": "Paneles compartidos", - "deletedPrivateBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al elegir \"Eliminar solo el panel\", las imágenes se colocan en un estado privado y sin categoría para el creador de la imagen.", + "deletedPrivateBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al elegir \"Eliminar solo el panel\", las imágenes se colocarán en un estado privado y sin categoría para el creador de la imagen.", "private": "Paneles privados", - "updateBoardError": "No se pudo actualizar el panel" + "updateBoardError": "No se pudo actualizar el panel", + "pause": "Pausa", + "resume": "Reanudar", + "restartFailed": "Reinicio fallido", + "restartFile": "Reiniciar archivo", + "restartRequired": "Reinicio requerido", + "resumeRefused": "Reanudación rechazada por el servidor. Reinicio requerido.", + "uncategorizedImages": "Imágenes sin categoría", + "deleteAllUncategorizedImages": "Eliminar todas las imágenes sin categoría", + "deletedImagesCannotBeRestored": "Las imágenes eliminadas no pueden ser restauradas.", + "hideBoards": "Ocultar tableros", + "locateInGalery": "Ubicar en galeria", + "viewBoards": "Ver paneles" }, "accordions": { "compositing": { @@ -867,5 +881,100 @@ "noModelsInstalled": "Parece que no tienes ningún modelo instalado", "gettingStartedSeries": "¿Desea más orientación? Consulte nuestra Serie de introducción para obtener consejos sobre cómo aprovechar todo el potencial de Invoke Studio.", "toGetStartedLocal": "Para empezar, asegúrate de descargar o importar los modelos necesarios para ejecutar Invoke. A continuación, introduzca un mensaje en el cuadro y haga clic en Invocar para generar su primera imagen. Seleccione una plantilla para mejorar los resultados. Puede elegir guardar sus imágenes directamente en Galería o editarlas en el Lienzo." + }, + "auth": { + "login": { + "title": "Iniciar sesión en InvokeAI", + "email": "Email", + "emailPlaceholder": "Email", + "password": "Contraseña", + "passwordPlaceholder": "Contraseña", + "rememberMe": "Recordarme por 7 días", + "signIn": "Iniciar sesión", + "signingIn": "Iniciando sesión...", + "loginFailed": "Inicio de sesión fallido. Por favor revise sus credenciales." + }, + "setup": { + "title": "Bienvenido a InvokeAI", + "subtitle": "Configure su cuenta de administrador para empezar", + "email": "Email", + "emailPlaceholder": "admin@example.com", + "emailHelper": "Este será su nombre de usuario para iniciar sesión", + "displayName": "Nombre para mostrar", + "displayNamePlaceholder": "Administrador", + "displayNameHelper": "Su nombre como se mostrará en la aplicación", + "password": "Contraseña", + "passwordPlaceholder": "Contraseña", + "passwordHelper": "Debe tener al menos 8 caracteres con mayúsculas, minúsculas y números", + "passwordTooShort": "La contraseña debe tener al menos 8 caracteres", + "passwordMissingRequirements": "La contraseña debe contener mayúsculas, minúsculas y numeros", + "confirmPassword": "Confirmar contraseña", + "confirmPasswordPlaceholder": "Confirmar contraseña", + "passwordsDoNotMatch": "Las contraseñas no coinciden", + "createAccount": "Crear cuenta de administrador", + "creatingAccount": "Configurando...", + "setupFailed": "Configuración fallida. Por favor intente nuevamente.", + "passwordHelperRelaxed": "Ingrese una contraseña (se mostrará la fortaleza)" + }, + "userMenu": "Menu de usuario", + "admin": "Administrador", + "logout": "Cerrar Sesión", + "adminOnlyFeature": "Esta funcionalidad solo esta disponible para administradores.", + "profile": { + "menuItem": "Mi perfil", + "title": "Mi perfil", + "email": "Email", + "emailReadOnly": "La dirección de email no puede ser cambiada", + "displayName": "Nombre para mostrar", + "displayNamePlaceholder": "Su nombre", + "changePassword": "Cambiar contraseña", + "currentPassword": "Contraseña Actual", + "currentPasswordPlaceholder": "Contraseña Actual", + "newPassword": "Nueva contraseña", + "newPasswordPlaceholder": "Nueva contraseña", + "confirmPassword": "Confirmar nueva contraseña", + "confirmPasswordPlaceholder": "Confirmar nueva contraseña", + "passwordsDoNotMatch": "Las contraseñas no coinciden", + "saveSuccess": "Perfil actualizado correctamente", + "saveFailed": "Falló el guardado del perfil. Por favor intente nuevamente." + }, + "userManagement": { + "menuItem": "Administración de usuario", + "title": "Administración de usuario", + "email": "Email", + "emailPlaceholder": "user@example.com", + "displayName": "Nombre para mostrar", + "displayNamePlaceholder": "Nombre para mostrar", + "password": "Contraseña", + "passwordPlaceholder": "Contraseña", + "newPassword": "Nueva contraseña", + "newPasswordPlaceholder": "Deje en blanco para conservar la contraseña actual", + "role": "Rol", + "status": "Estado", + "actions": "Acciones", + "isAdmin": "Administrador", + "user": "Usuario", + "you": "Tu", + "createUser": "Crear usuario", + "editUser": "Editar usuario", + "deleteUser": "Eliminar usuario", + "deleteConfirm": "Esta seguro que desea eliminar {{name}}? Esta accion no se podrá revertir.", + "generatePassword": "Generar contraseña robusta", + "showPassword": "Mostrar contraseña", + "hidePassword": "Ocultar contraseña", + "activate": "Activar", + "deactivate": "Desactivar", + "saveFailed": "Fallo al guardar usuario. Por favor intente nuevamente.", + "deleteFailed": "Fallo al borrar usuario. Por favor intente nuevamente.", + "loadFailed": "Fallo al cargar usuarios.", + "back": "Atras", + "cannotDeleteSelf": "Usted no puede eliminar su propia cuenta", + "cannotDeactivateSelf": "Usted no puede desactivar su propia cuenta" + }, + "passwordStrength": { + "weak": "Contraseña debil", + "moderate": "Contraseña moderada", + "strong": "Contraseña fuerte" + } } } diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index cafd91d2a4..759a17616f 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -3009,9 +3009,9 @@ "readReleaseNotes": "Leggi le note di rilascio", "watchRecentReleaseVideos": "Guarda i video su questa versione", "items": [ - "Supporto FLUX.2 Klein: InvokeAI ora supporta i nuovi modelli FLUX.2 Klein (varianti 4B e 9B) con formati GGUF, FP8 e Diffusers. Le funzionalità includono txt2img, img2img, inpainting e outpainting. Consultare la sezione \"Modelli di partenza\" per iniziare.", - "Il supporto DyPE per i modelli FLUX migliora le immagini ad alta risoluzione (da >1536 px fino a 4K). Vai alla sezione \"Opzioni avanzate\" per attivarlo.", - "Diversità Z-Image Turbo: attiva 'Seed Variance Enhancer' in 'Opzioni avanzate' per aggiungere diversità alle tue generazioni ZiT." + "La modalità multiutente supporta più utenti isolati sullo stesso server.", + "Supporto migliorato per i modelli Z-Image e FLUX.2.", + "Numerosi miglioramenti dell'interfaccia utente e nuove funzionalità Tela." ], "watchUiUpdatesOverview": "Guarda la panoramica degli aggiornamenti dell'interfaccia utente", "takeUserSurvey": "📣 Facci sapere cosa ne pensi di InvokeAI. Partecipa al nostro sondaggio sull'esperienza utente!" diff --git a/invokeai/frontend/web/public/locales/ja.json b/invokeai/frontend/web/public/locales/ja.json index 7acf6c98ca..ed8e438693 100644 --- a/invokeai/frontend/web/public/locales/ja.json +++ b/invokeai/frontend/web/public/locales/ja.json @@ -157,7 +157,7 @@ "noImageSelected": "画像が選択されていません", "deleteSelection": "選択中のものを削除", "downloadSelection": "選択中のものをダウンロード", - "starImage": "スターをつける", + "starImage": "スター", "viewerImage": "閲覧画像", "compareImage": "比較画像", "openInViewer": "ビューアで開く", @@ -190,7 +190,7 @@ "selectAllOnPage": "ページ上のすべてを選択", "images": "画像", "assetsTab": "プロジェクトで使用するためにアップロードされたファイル。", - "imagesTab": "Invoke内で作成および保存された画像。", + "imagesTab": "Invoke内であなたが作成および保存した画像。", "assets": "アセット", "useForPromptGeneration": "プロンプト生成に使用する", "jump": "ジャンプ", @@ -198,7 +198,8 @@ "unableToLoad": "ギャラリーを読み込めません", "selectAnImageToCompare": "比較する画像を選択", "openViewer": "ビューアーを開く", - "closeViewer": "ビューアーを閉じる" + "closeViewer": "ビューアーを閉じる", + "usePagedGalleryView": "ページ型ギャラリービューを使う" }, "hotkeys": { "searchHotkeys": "ホットキーを検索", @@ -515,6 +516,9 @@ "title": "生成タブを選択", "desc": "生成タブを選択。", "key": "1" + }, + "promptWeightUp": { + "title": "選択したプロンプトの重みを増加" } }, "hotkeys": "ホットキー", @@ -568,7 +572,30 @@ "title": "画像にスターを付ける/スターを外す", "desc": "選択した画像にスターを付けたり、スターを外したりします。" } - } + }, + "editMode": "編集モード", + "viewMode": "ビューモード", + "editHotkey": "ホットキーの編集", + "addHotkey": "ホットキーの追加", + "resetToDefault": "デフォルトにリセット", + "resetAll": "全てをデフォルトにリセット", + "resetAllConfirmation": "すべてのホットキーをデフォルトに戻してよろしいですか?この操作は取り消せません。", + "enterHotkeys": "カンマ区切りでホットキーを入力してください", + "save": "保存", + "cancel": "キャンセル", + "modifiers": "モディファイア", + "syntaxHelp": "構文のヘルプ", + "multipleHotkeys": "カンマで区切られた複数のホットキー", + "help": "ヘルプ", + "noHotkeysRecorded": "まだホットキーが記録されていません", + "pressKeys": "キーを押してください...", + "setHotkey": "セット", + "setAnother": "他をセット", + "removeLastHotkey": "最後のホットキーを削除", + "clearAll": "全てをクリア", + "duplicateWarning": "このホットキーはすでに記録済みです", + "conflictWarning": "はすでに \"{{hotkeyTitle}}\" で使われています", + "thisHotkey": "このホットキー" }, "modelManager": { "modelManager": "モデルマネージャ", @@ -1083,7 +1110,7 @@ "batchQueuedDesc_other": "{{count}} セッションをキューの{{direction}}に追加しました", "graphQueued": "グラフをキューに追加しました", "batch": "バッチ", - "clearQueueAlertDialog": "キューをクリアすると、処理中の項目は直ちにキャンセルされ、キューは完全にクリアされます。保留中のフィルターもキャンセルされます。", + "clearQueueAlertDialog": "キューをクリアすると、処理中の項目は直ちにキャンセルされ、キューは完全にクリアされます。保留中のフィルターもキャンセルされ、ステージングエリアもリセットされます。", "pending": "保留中", "resumeFailed": "処理の再開に問題があります", "clear": "クリア", @@ -1135,7 +1162,13 @@ "sortColumn": "列の並べ替え", "sortBy": "{{column}}で並べ替え", "sortOrderAscending": "昇順", - "sortOrderDescending": "降順" + "sortOrderDescending": "降順", + "cancelFailedAccessDenied": "アイテムのキャンセル中に問題が発生しました:アクセスが拒否されました", + "clearFailedAccessDenied": "キューのクリア中に問題が発生しました:アクセスが拒否されました", + "paused": "一時停止中", + "user": "ユーザー", + "fieldValuesHidden": "<非表示>", + "cannotViewDetails": "このキューアイテムを閲覧する権限がありません" }, "models": { "noMatchingModels": "一致するモデルがありません", @@ -1381,7 +1414,13 @@ "deletedImagesCannotBeRestored": "削除された画像は復元できません。", "hideBoards": "ボードを隠す", "locateInGalery": "ギャラリーで検索", - "viewBoards": "ボードを表示" + "viewBoards": "ボードを表示", + "pause": "一時停止", + "resume": "再開", + "restartFailed": "再起動に失敗しました", + "restartFile": "ファイルを再起動", + "restartRequired": "再起動が必要です", + "resumeRefused": "サーバーで再開が拒否されました。再起動が必要です。" }, "invocationCache": { "invocationCache": "呼び出しキャッシュ", @@ -2786,5 +2825,100 @@ }, "lora": { "weight": "重み" + }, + "auth": { + "login": { + "title": "Invokeにサインイン", + "email": "Eメール", + "emailPlaceholder": "Eメール", + "password": "パスワード", + "passwordPlaceholder": "パスワード", + "rememberMe": "7日間は記憶", + "signIn": "サインイン", + "signingIn": "サインイン中...", + "loginFailed": "ログインに失敗しました。正しい内容かを確認してください。" + }, + "setup": { + "title": "Invokeへようこそ", + "subtitle": "管理者アカウントをセットアップします", + "email": "Eメール", + "emailPlaceholder": "hoge@example.com", + "emailHelper": "これはサインインに使うユーザー名になります", + "displayName": "表示名", + "displayNamePlaceholder": "管理者", + "displayNameHelper": "アプリケーションの中で表示される名前です", + "password": "パスワード", + "passwordPlaceholder": "パスワード", + "passwordHelper": "大文字、小文字、数字を組み合わせた8文字以上", + "passwordTooShort": "パスワードは8文字以上である必要があります", + "passwordMissingRequirements": "パスワードは小文字、大文字、数字を含まなければなりません", + "confirmPassword": "パスワードの確認", + "confirmPasswordPlaceholder": "パスワードの確認", + "passwordsDoNotMatch": "パスワードが一致しません", + "createAccount": "管理者アカウントを作る", + "creatingAccount": "設定中...", + "setupFailed": "セットアップに失敗しました。もう一度試してください。", + "passwordHelperRelaxed": "パスワードを入力してください(強度が表示されます)" + }, + "userMenu": "ユーザーメニュー", + "admin": "管理", + "logout": "ログアウト", + "adminOnlyFeature": "この機能は管理者のみ使用できます。", + "profile": { + "menuItem": "プロフィール", + "title": "プロフィール", + "email": "Eメール", + "emailReadOnly": "Eメールアドレスは変更できません", + "displayName": "表示名", + "displayNamePlaceholder": "あなたの名前", + "changePassword": "パスワードの変更", + "currentPassword": "現在のパスワード", + "currentPasswordPlaceholder": "現在のパスワード", + "newPassword": "新しいパスワード", + "newPasswordPlaceholder": "新しいパスワード", + "confirmPassword": "新しいパスワードの確認", + "confirmPasswordPlaceholder": "新しいパスワードの確認", + "passwordsDoNotMatch": "パスワードが一致しません", + "saveSuccess": "プロフィールのアップデートに成功しました", + "saveFailed": "プロフィールの保存に失敗しました。もう一度試してください。" + }, + "userManagement": { + "menuItem": "ユーザー管理", + "title": "ユーザー管理", + "email": "Eメール", + "emailPlaceholder": "hoge@example.com", + "displayName": "表示名", + "displayNamePlaceholder": "表示名", + "password": "パスワード", + "passwordPlaceholder": "パスワード", + "newPassword": "新しいパスワード", + "newPasswordPlaceholder": "現在のパスワードを維持するには空白にしておいてください", + "role": "ロール", + "status": "ステータス", + "actions": "アクション", + "isAdmin": "管理者", + "user": "ユーザー", + "you": "あなた", + "createUser": "ユーザーの作成", + "editUser": "ユーザーの編集", + "deleteUser": "ユーザーの削除", + "deleteConfirm": "本当に \"{{name}}\" を削除しますか?このアクションは取り消せません。", + "generatePassword": "強力なパスワードを生成", + "showPassword": "パスワードの表示", + "hidePassword": "パスワードを隠す", + "activate": "有効化", + "deactivate": "非有効化", + "saveFailed": "ユーザーの保存に失敗しました。もう一度実行してください。", + "deleteFailed": "ユーザーの削除に失敗しました。もう一度実行してください。", + "loadFailed": "ユーザーのロードに失敗しました。", + "back": "戻る", + "cannotDeleteSelf": "あなた自身のアカウントを削除することはできません", + "cannotDeactivateSelf": "あなた自身のアカウントを非有効化することはできません" + }, + "passwordStrength": { + "weak": "弱いパスワード", + "moderate": "適切なパスワード", + "strong": "強力なパスワード" + } } } From c2016bcfb7d4edebbd72424365a656bcfb9d285a Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Mon, 6 Apr 2026 23:13:10 +0200 Subject: [PATCH 25/56] feat: full canvas workflow integration for external models - Add missing aspect ratios (4:5, 5:4, 8:1, 4:1, 1:4, 1:8) to type system for external model support - Sync canvas bbox when external model resolution preset is selected - Use params preset dimensions in buildExternalGraph to prevent "unsupported aspect ratio" errors - Lock all bbox controls (resize handles, aspect ratio select, width/height sliders, swap/optimal buttons) for external models with fixed dimension presets - Disable denoise strength slider for external models (not applicable) - Sync bbox aspect ratio changes back to paramsSlice for external models - Initialize bbox dimensions when switching to an external model --- invokeai/frontend/web/public/locales/en.json | 1 + .../listeners/modelSelected.ts | 34 ++++++++++- .../components/ParamDenoisingStrength.tsx | 13 ++++- .../konva/CanvasTool/CanvasBboxToolModule.ts | 9 ++- .../controlLayers/store/canvasSlice.ts | 56 +++++++++++++++---- .../src/features/controlLayers/store/types.ts | 25 ++++++++- .../graph/generation/buildExternalGraph.ts | 16 +++--- .../components/Bbox/BboxAspectRatioSelect.tsx | 24 ++++++-- .../Bbox/BboxSwapDimensionsButton.tsx | 6 +- .../Bbox/use-is-bbox-size-locked.ts | 5 +- 10 files changed, 153 insertions(+), 36 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 4686ad070e..c13020c608 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1490,6 +1490,7 @@ "copyImage": "Copy Image", "denoisingStrength": "Denoising Strength", "disabledNoRasterContent": "Disabled (No Raster Content)", + "disabledNotSupported": "Not supported by model", "downloadImage": "Download Image", "general": "General", "guidance": "Guidance", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index ed2c67d529..25bad13f4b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -4,9 +4,11 @@ import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/c import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; import { + aspectRatioIdChanged, kleinQwen3EncoderModelSelected, kleinVaeModelSelected, modelChanged, + resolutionPresetSelected, setZImageScheduler, syncedToOptimalDimension, vaeSelected, @@ -24,7 +26,7 @@ import { selectBboxModelBase, selectCanvasSlice, } from 'features/controlLayers/store/selectors'; -import { getEntityIdentifier, isFlux2ReferenceImageConfig } from 'features/controlLayers/store/types'; +import { getEntityIdentifier, isAspectRatioID, isFlux2ReferenceImageConfig } from 'features/controlLayers/store/types'; import { initialFlux2ReferenceImage, initialFluxKontextReferenceImage, @@ -46,7 +48,7 @@ import { selectZImageDiffusersModels, } from 'services/api/hooks/modelsByType'; import type { FLUXKontextModelConfig, FLUXReduxModelConfig, IPAdapterModelConfig } from 'services/api/types'; -import { isFluxKontextModelConfig, isFluxReduxModelConfig } from 'services/api/types'; +import { isExternalApiModelConfig, isFluxKontextModelConfig, isFluxReduxModelConfig } from 'services/api/types'; const log = logger('models'); @@ -352,6 +354,34 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = dispatch(bboxSyncedToOptimalDimension()); } } + + // When switching to an external model, sync bbox to the model's first preset dimensions + if (newBase === 'external') { + const modelConfigsResult = selectModelConfigsQuery(getState()); + if (modelConfigsResult.data) { + const newModelConfig = modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, newModel.key); + if (newModelConfig && isExternalApiModelConfig(newModelConfig)) { + const { aspect_ratio_sizes, resolution_presets } = newModelConfig.capabilities; + if (resolution_presets && resolution_presets.length > 0) { + const firstPreset = resolution_presets[0]!; + dispatch( + resolutionPresetSelected({ + imageSize: firstPreset.image_size, + aspectRatio: firstPreset.aspect_ratio, + width: firstPreset.width, + height: firstPreset.height, + }) + ); + } else if (aspect_ratio_sizes) { + const firstRatio = Object.keys(aspect_ratio_sizes)[0]; + const firstSize = firstRatio ? aspect_ratio_sizes[firstRatio] : undefined; + if (firstRatio && firstSize && isAspectRatioID(firstRatio)) { + dispatch(aspectRatioIdChanged({ id: firstRatio, fixedSize: firstSize })); + } + } + } + } + } }, }); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx index 34fb96f063..658fb8b745 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx @@ -11,7 +11,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import WavyLine from 'common/components/WavyLine'; -import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice'; +import { selectImg2imgStrength, selectIsExternal, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice'; import { selectActiveRasterLayerEntities } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -37,6 +37,7 @@ export const ParamDenoisingStrength = memo(() => { const img2imgStrength = useAppSelector(selectImg2imgStrength); const dispatch = useAppDispatch(); const hasRasterLayersWithContent = useAppSelector(selectHasRasterLayersWithContent); + const isExternal = useAppSelector(selectIsExternal); const selectedModelConfig = useSelectedModelConfig(); const onChange = useCallback( @@ -55,12 +56,16 @@ export const ParamDenoisingStrength = memo(() => { // Denoising strength does nothing if there are no raster layers w/ content return true; } + if (isExternal) { + // External models don't support denoise strength - they handle img2img via prompt + return true; + } if (selectedModelConfig && isFluxFillMainModelModelConfig(selectedModelConfig)) { // Denoising strength is ignored by FLUX Fill, which is indicated by the variant being 'inpaint' return true; } return false; - }, [hasRasterLayersWithContent, selectedModelConfig]); + }, [hasRasterLayersWithContent, isExternal, selectedModelConfig]); return ( @@ -96,7 +101,9 @@ export const ParamDenoisingStrength = memo(() => { ) : ( - {t('parameters.disabledNoRasterContent')} + + {isExternal ? t('parameters.disabledNotSupported') : t('parameters.disabledNoRasterContent')} + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBboxToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBboxToolModule.ts index ecf9a5d1c7..2ab2d1f281 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBboxToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBboxToolModule.ts @@ -10,7 +10,7 @@ import { getPrefixedId, } from 'features/controlLayers/konva/util'; import { selectBboxOverlay } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectModel } from 'features/controlLayers/store/paramsSlice'; +import { selectHasFixedDimensionSizes, selectModel } from 'features/controlLayers/store/paramsSlice'; import { selectBbox } from 'features/controlLayers/store/selectors'; import type { Coordinate, Rect, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -191,6 +191,9 @@ export class CanvasBboxToolModule extends CanvasModuleBase { // Listen for the model changing - some model types constraint the bbox to a certain size or aspect ratio. this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectModel, this.render)); + // Listen for fixed dimension sizes changes - external models may lock bbox resizing + this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectHasFixedDimensionSizes, this.render)); + // Update on busy state changes this.subscriptions.add(this.manager.$isBusy.listen(this.render)); @@ -246,6 +249,10 @@ export class CanvasBboxToolModule extends CanvasModuleBase { if (tool !== 'bbox') { return NO_ANCHORS; } + // External models with fixed dimension presets don't allow free bbox resizing + if (this.manager.stateApi.runSelector(selectHasFixedDimensionSizes)) { + return NO_ANCHORS; + } return ALL_ANCHORS; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 79d3963d12..9c283f188f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -7,7 +7,7 @@ import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMul import { merge } from 'es-toolkit/compat'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { canvasReset } from 'features/controlLayers/store/actions'; -import { modelChanged } from 'features/controlLayers/store/paramsSlice'; +import { aspectRatioIdChanged, modelChanged, resolutionPresetSelected } from 'features/controlLayers/store/paramsSlice'; import { selectAllEntities, selectAllEntitiesOfType, @@ -31,6 +31,7 @@ import type { RgbColor, SimpleAdjustmentsConfig, } from 'features/controlLayers/store/types'; +import { isAspectRatioID } from 'features/controlLayers/store/types'; import { calculateNewSize, getScaledBoundingBoxDimensions, @@ -1279,21 +1280,31 @@ const slice = createSlice({ state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked; syncScaledSize(state); }, - bboxAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { - const { id } = action.payload; + bboxAspectRatioIdChanged: ( + state, + action: PayloadAction<{ id: AspectRatioID; fixedSize?: { width: number; height: number } }> + ) => { + const { id, fixedSize } = action.payload; state.bbox.aspectRatio.id = id; if (id === 'Free') { state.bbox.aspectRatio.isLocked = false; } else { state.bbox.aspectRatio.isLocked = true; - state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; - const { width, height } = calculateNewSize( - state.bbox.aspectRatio.value, - state.bbox.rect.width * state.bbox.rect.height, - state.bbox.modelBase - ); - state.bbox.rect.width = width; - state.bbox.rect.height = height; + if (fixedSize) { + // External models provide fixed dimensions for each aspect ratio + state.bbox.aspectRatio.value = fixedSize.width / fixedSize.height; + state.bbox.rect.width = fixedSize.width; + state.bbox.rect.height = fixedSize.height; + } else { + state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; + const { width, height } = calculateNewSize( + state.bbox.aspectRatio.value, + state.bbox.rect.width * state.bbox.rect.height, + state.bbox.modelBase + ); + state.bbox.rect.width = width; + state.bbox.rect.height = height; + } } syncScaledSize(state); @@ -1744,6 +1755,29 @@ const slice = createSlice({ syncScaledSize(state); } }); + // Sync bbox when external model resolution preset is selected (aspect_ratio_sizes) + builder.addCase(aspectRatioIdChanged, (state, action) => { + const { id, fixedSize } = action.payload; + // Only sync when fixedSize is provided (external models with aspect_ratio_sizes) + if (fixedSize) { + state.bbox.rect.width = fixedSize.width; + state.bbox.rect.height = fixedSize.height; + state.bbox.aspectRatio.value = fixedSize.width / fixedSize.height; + state.bbox.aspectRatio.id = id; + state.bbox.aspectRatio.isLocked = true; + syncScaledSize(state); + } + }); + // Sync bbox when external model resolution preset is selected (resolution_presets) + builder.addCase(resolutionPresetSelected, (state, action) => { + const { width, height, aspectRatio } = action.payload; + state.bbox.rect.width = width; + state.bbox.rect.height = height; + state.bbox.aspectRatio.value = width / height; + state.bbox.aspectRatio.id = isAspectRatioID(aspectRatio) ? aspectRatio : 'Free'; + state.bbox.aspectRatio.isLocked = true; + syncScaledSize(state); + }); }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 77ad6619db..2b09b4b8ed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -636,19 +636,42 @@ export const zLoRA = z.object({ }); export type LoRA = z.infer; -export const zAspectRatioID = z.enum(['Free', '21:9', '16:9', '3:2', '4:3', '1:1', '3:4', '2:3', '9:16', '9:21']); +export const zAspectRatioID = z.enum([ + 'Free', + '8:1', + '4:1', + '21:9', + '16:9', + '3:2', + '5:4', + '4:3', + '1:1', + '3:4', + '4:5', + '2:3', + '9:16', + '1:4', + '9:21', + '1:8', +]); export type AspectRatioID = z.infer; export const isAspectRatioID = (v: unknown): v is AspectRatioID => zAspectRatioID.safeParse(v).success; export const ASPECT_RATIO_MAP: Record, { ratio: number; inverseID: AspectRatioID }> = { + '8:1': { ratio: 8 / 1, inverseID: '1:8' }, + '4:1': { ratio: 4 / 1, inverseID: '1:4' }, '21:9': { ratio: 21 / 9, inverseID: '9:21' }, '16:9': { ratio: 16 / 9, inverseID: '9:16' }, '3:2': { ratio: 3 / 2, inverseID: '2:3' }, + '5:4': { ratio: 5 / 4, inverseID: '4:5' }, '4:3': { ratio: 4 / 3, inverseID: '4:3' }, '1:1': { ratio: 1, inverseID: '1:1' }, '3:4': { ratio: 3 / 4, inverseID: '4:3' }, + '4:5': { ratio: 4 / 5, inverseID: '5:4' }, '2:3': { ratio: 2 / 3, inverseID: '3:2' }, '9:16': { ratio: 9 / 16, inverseID: '16:9' }, + '1:4': { ratio: 1 / 4, inverseID: '4:1' }, '9:21': { ratio: 9 / 21, inverseID: '21:9' }, + '1:8': { ratio: 1 / 8, inverseID: '8:1' }, }; const zAspectRatioConfig = z.object({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts index 0ba82234a6..2d7ee19897 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts @@ -6,7 +6,6 @@ import { type ModelIdentifierField, zImageField } from 'features/nodes/types/com import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getOriginalAndScaledSizesForOtherModes, - getOriginalAndScaledSizesForTextToImage, selectCanvasOutputFields, } from 'features/nodes/util/graph/graphBuilderUtils'; import { @@ -110,16 +109,15 @@ export const buildExternalGraph = async (arg: GraphBuilderArg): Promise { const id = useAppSelector(selectAspectRatioID); const isStaging = useCanvasIsStaging(); const allowedAspectRatios = useAppSelector(selectAllowedAspectRatioIDs); - const options = allowedAspectRatios ?? zAspectRatioID.options; + const aspectRatioSizes = useAppSelector(selectAspectRatioSizes); + const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes); + const options = useMemo(() => allowedAspectRatios ?? zAspectRatioID.options, [allowedAspectRatios]); const onChange = useCallback>( (e) => { if (!isAspectRatioID(e.target.value)) { return; } - dispatch(bboxAspectRatioIdChanged({ id: e.target.value })); + const fixedSize = aspectRatioSizes?.[e.target.value] ?? undefined; + dispatch(bboxAspectRatioIdChanged({ id: e.target.value, fixedSize })); + // For external models with fixed sizes, also sync to params so buildExternalGraph uses correct dimensions + if (fixedSize) { + dispatch(aspectRatioIdChanged({ id: e.target.value, fixedSize })); + } }, - [dispatch] + [dispatch, aspectRatioSizes] ); return ( - + {t('parameters.aspect')} diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx index 54614419a5..372f8187ea 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx @@ -1,7 +1,8 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { bboxDimensionsSwapped } from 'features/controlLayers/store/canvasSlice'; import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectHasFixedDimensionSizes } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsDownUpBold } from 'react-icons/pi'; @@ -10,6 +11,7 @@ export const BboxSwapDimensionsButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isStaging = useCanvasIsStaging(); + const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes); const onClick = useCallback(() => { dispatch(bboxDimensionsSwapped()); }, [dispatch]); @@ -21,7 +23,7 @@ export const BboxSwapDimensionsButton = memo(() => { variant="ghost" size="sm" icon={} - isDisabled={isStaging} + isDisabled={isStaging || hasFixedSizes} /> ); }); diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts b/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts index eaf1381108..18f453708e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts @@ -1,6 +1,9 @@ +import { useAppSelector } from 'app/store/storeHooks'; import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectHasFixedDimensionSizes } from 'features/controlLayers/store/paramsSlice'; export const useIsBboxSizeLocked = () => { const isStaging = useCanvasIsStaging(); - return isStaging; + const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes); + return isStaging || hasFixedSizes; }; From 089e2db402effa338bfe6e9b918851bcb967f83d Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Mon, 6 Apr 2026 23:21:45 +0200 Subject: [PATCH 26/56] Chore typegen Linux seperator --- invokeai/frontend/web/src/services/api/schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 2729d86f70..6981dde09b 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -15188,14 +15188,14 @@ export type components = { * Convert Cache Dir * Format: path * @description Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions). - * @default models\.convert_cache + * @default models/.convert_cache */ convert_cache_dir?: string; /** * Download Cache Dir * Format: path * @description Path to the directory that contains dynamically downloaded models. - * @default models\.download_cache + * @default models/.download_cache */ download_cache_dir?: string; /** From 3e9e052d5db60329f5f3448920e0e855db69b0fc Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Mon, 6 Apr 2026 23:32:10 +0200 Subject: [PATCH 27/56] feat: full canvas workflow integration for external models - Update buildExternalGraph test to include dimensions in mock params --- .../nodes/util/graph/generation/buildExternalGraph.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts index 561453fc4e..f1fa54b4e0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts @@ -82,6 +82,7 @@ beforeEach(() => { mockParams = { steps: 20, guidance: 4.5, + dimensions: { width: 768, height: 512, aspectRatio: { id: '3:2', value: 1.5, isLocked: true } }, } as ParamsState; mockSizes = { scaledSize: { width: 768, height: 512 } }; From ae42182246dfb28da96798d4e54d2c96109ea4d9 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Tue, 7 Apr 2026 03:52:06 +0200 Subject: [PATCH 28/56] fix: detect Z-Image LoRAs with transformer.layers prefix (#8986) OneTrainer exports Z-Image LoRAs with 'transformer.layers.' key prefix instead of 'diffusion_model.layers.'. Add this prefix (and the PEFT-wrapped 'base_model.model.transformer.layers.' variant) to the Z-Image LoRA probe so these models are correctly identified and loaded. --- invokeai/backend/model_manager/configs/lora.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invokeai/backend/model_manager/configs/lora.py b/invokeai/backend/model_manager/configs/lora.py index 1619c9d6f0..791ded2ed0 100644 --- a/invokeai/backend/model_manager/configs/lora.py +++ b/invokeai/backend/model_manager/configs/lora.py @@ -711,6 +711,8 @@ class LoRA_LyCORIS_ZImage_Config(LoRA_LyCORIS_Config_Base, Config_Base): state_dict, { "diffusion_model.layers.", # Z-Image S3-DiT layer pattern + "transformer.layers.", # OneTrainer/diffusers prefix variant + "base_model.model.transformer.layers.", # PEFT-wrapped variant }, ) @@ -747,6 +749,8 @@ class LoRA_LyCORIS_ZImage_Config(LoRA_LyCORIS_Config_Base, Config_Base): state_dict, { "diffusion_model.layers.", # Z-Image S3-DiT layer pattern + "transformer.layers.", # OneTrainer/diffusers prefix variant + "base_model.model.transformer.layers.", # PEFT-wrapped variant }, ) From f08b8029682fbcbbef0163944b2468cf40a085a6 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Tue, 7 Apr 2026 04:04:48 +0200 Subject: [PATCH 29/56] feat: add support for OneTrainer BFL Flux LoRA format (#8984) * feat: add support for OneTrainer BFL Flux LoRA format Newer versions of OneTrainer export Flux LoRAs using BFL internal key names (double_blocks, single_blocks, img_attn, etc.) with a 'transformer.' prefix and split QKV projections (qkv.0/1/2, linear1.0/1/2/3). This format was not recognized by any existing detector. Add detection and conversion for this format, merging split QKV and linear1 layers into MergedLayerPatch instances for the fused BFL model. * chore ruff --- .../model_manager/load/model_loaders/lora.py | 6 + invokeai/backend/model_manager/taxonomy.py | 1 + ...ux_onetrainer_bfl_lora_conversion_utils.py | 168 ++++++++++++++++++ .../patches/lora_conversions/formats.py | 5 + 4 files changed, 180 insertions(+) create mode 100644 invokeai/backend/patches/lora_conversions/flux_onetrainer_bfl_lora_conversion_utils.py diff --git a/invokeai/backend/model_manager/load/model_loaders/lora.py b/invokeai/backend/model_manager/load/model_loaders/lora.py index d39982456a..67d862a01d 100644 --- a/invokeai/backend/model_manager/load/model_loaders/lora.py +++ b/invokeai/backend/model_manager/load/model_loaders/lora.py @@ -44,6 +44,10 @@ from invokeai.backend.patches.lora_conversions.flux_kohya_lora_conversion_utils is_state_dict_likely_in_flux_kohya_format, lora_model_from_flux_kohya_state_dict, ) +from invokeai.backend.patches.lora_conversions.flux_onetrainer_bfl_lora_conversion_utils import ( + is_state_dict_likely_in_flux_onetrainer_bfl_format, + lora_model_from_flux_onetrainer_bfl_state_dict, +) from invokeai.backend.patches.lora_conversions.flux_onetrainer_lora_conversion_utils import ( is_state_dict_likely_in_flux_onetrainer_format, lora_model_from_flux_onetrainer_state_dict, @@ -128,6 +132,8 @@ class LoRALoader(ModelLoader): model = lora_model_from_flux_diffusers_state_dict(state_dict=state_dict, alpha=None) elif is_state_dict_likely_in_flux_kohya_format(state_dict=state_dict): model = lora_model_from_flux_kohya_state_dict(state_dict=state_dict) + elif is_state_dict_likely_in_flux_onetrainer_bfl_format(state_dict=state_dict): + model = lora_model_from_flux_onetrainer_bfl_state_dict(state_dict=state_dict) elif is_state_dict_likely_in_flux_onetrainer_format(state_dict=state_dict): model = lora_model_from_flux_onetrainer_state_dict(state_dict=state_dict) elif is_state_dict_likely_flux_control(state_dict=state_dict): diff --git a/invokeai/backend/model_manager/taxonomy.py b/invokeai/backend/model_manager/taxonomy.py index c002418a6b..9dc0da7733 100644 --- a/invokeai/backend/model_manager/taxonomy.py +++ b/invokeai/backend/model_manager/taxonomy.py @@ -210,6 +210,7 @@ class FluxLoRAFormat(str, Enum): AIToolkit = "flux.aitoolkit" XLabs = "flux.xlabs" BflPeft = "flux.bfl_peft" + OneTrainerBfl = "flux.onetrainer_bfl" AnyVariant: TypeAlias = Union[ diff --git a/invokeai/backend/patches/lora_conversions/flux_onetrainer_bfl_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/flux_onetrainer_bfl_lora_conversion_utils.py new file mode 100644 index 0000000000..b2109222a3 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/flux_onetrainer_bfl_lora_conversion_utils.py @@ -0,0 +1,168 @@ +"""Utilities for detecting and converting FLUX LoRAs in OneTrainer BFL format. + +This format is produced by newer versions of OneTrainer and uses BFL internal key names +(double_blocks, single_blocks, img_attn, etc.) with a 'transformer.' prefix and +InvokeAI-native LoRA suffixes (lora_down.weight, lora_up.weight, alpha). + +Unlike the standard BFL PEFT format (which uses 'diffusion_model.' prefix and lora_A/lora_B), +this format also has split QKV projections: + - double_blocks.{i}.img_attn.qkv.{0,1,2} (Q, K, V separate) + - double_blocks.{i}.txt_attn.qkv.{0,1,2} (Q, K, V separate) + - single_blocks.{i}.linear1.{0,1,2,3} (Q, K, V, MLP separate) + +Example keys: + transformer.double_blocks.0.img_attn.qkv.0.lora_down.weight + transformer.double_blocks.0.img_attn.qkv.0.lora_up.weight + transformer.double_blocks.0.img_attn.qkv.0.alpha + transformer.single_blocks.0.linear1.3.lora_down.weight + transformer.double_blocks.0.img_mlp.0.lora_down.weight +""" + +import re +from typing import Any, Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.merged_layer_patch import MergedLayerPatch, Range +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw + +_TRANSFORMER_PREFIX = "transformer." + +# Valid LoRA weight suffixes in this format. +_LORA_SUFFIXES = ("lora_down.weight", "lora_up.weight", "alpha") + +# Regex to detect split QKV keys in double blocks: e.g. "double_blocks.0.img_attn.qkv.1" +_SPLIT_QKV_RE = re.compile(r"^(double_blocks\.\d+\.(img_attn|txt_attn)\.qkv)\.\d+$") + +# Regex to detect split linear1 keys in single blocks: e.g. "single_blocks.0.linear1.2" +_SPLIT_LINEAR1_RE = re.compile(r"^(single_blocks\.\d+\.linear1)\.\d+$") + + +def is_state_dict_likely_in_flux_onetrainer_bfl_format( + state_dict: dict[str | int, Any], + metadata: dict[str, Any] | None = None, +) -> bool: + """Checks if the provided state dict is likely in the OneTrainer BFL FLUX LoRA format. + + This format uses BFL internal key names with 'transformer.' prefix and split QKV projections. + """ + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + if not str_keys: + return False + + # All keys must start with 'transformer.' + if not all(k.startswith(_TRANSFORMER_PREFIX) for k in str_keys): + return False + + # All keys must end with recognized LoRA suffixes. + if not all(k.endswith(_LORA_SUFFIXES) for k in str_keys): + return False + + # Must have BFL block structure (double_blocks or single_blocks) under transformer prefix. + has_bfl_blocks = any( + k.startswith("transformer.double_blocks.") or k.startswith("transformer.single_blocks.") for k in str_keys + ) + if not has_bfl_blocks: + return False + + # Must have split QKV pattern (qkv.0, qkv.1, qkv.2) to distinguish from other formats + # that might use transformer. prefix in the future. + has_split_qkv = any(".qkv.0." in k or ".qkv.1." in k or ".qkv.2." in k or ".linear1.0." in k for k in str_keys) + if not has_split_qkv: + return False + + return True + + +def _split_key(key: str) -> tuple[str, str]: + """Split a key into (layer_name, weight_suffix). + + Handles: + - 2-component suffixes ending with '.weight': e.g., 'lora_down.weight' → split at 2nd-to-last dot + - 1-component suffixes: e.g., 'alpha' → split at last dot + """ + if key.endswith(".weight"): + parts = key.rsplit(".", maxsplit=2) + return parts[0], f"{parts[1]}.{parts[2]}" + else: + parts = key.rsplit(".", maxsplit=1) + return parts[0], parts[1] + + +def lora_model_from_flux_onetrainer_bfl_state_dict(state_dict: Dict[str, torch.Tensor]) -> ModelPatchRaw: + """Convert a OneTrainer BFL format FLUX LoRA state dict to a ModelPatchRaw. + + Strips the 'transformer.' prefix, groups by layer, and merges split QKV/linear1 + layers into MergedLayerPatch instances. + """ + # Step 1: Strip prefix and group by layer name. + grouped_state_dict: dict[str, dict[str, torch.Tensor]] = {} + for key, value in state_dict.items(): + if not isinstance(key, str): + continue + + # Strip 'transformer.' prefix. + key = key[len(_TRANSFORMER_PREFIX) :] + + layer_name, suffix = _split_key(key) + + if layer_name not in grouped_state_dict: + grouped_state_dict[layer_name] = {} + grouped_state_dict[layer_name][suffix] = value + + # Step 2: Build LoRA layers, merging split QKV and linear1. + layers: dict[str, BaseLayerPatch] = {} + + # Identify which layers need merging. + merge_groups: dict[str, list[str]] = {} + standalone_keys: list[str] = [] + + for layer_key in grouped_state_dict: + qkv_match = _SPLIT_QKV_RE.match(layer_key) + linear1_match = _SPLIT_LINEAR1_RE.match(layer_key) + + if qkv_match: + parent = qkv_match.group(1) + if parent not in merge_groups: + merge_groups[parent] = [] + merge_groups[parent].append(layer_key) + elif linear1_match: + parent = linear1_match.group(1) + if parent not in merge_groups: + merge_groups[parent] = [] + merge_groups[parent].append(layer_key) + else: + standalone_keys.append(layer_key) + + # Process standalone layers. + for layer_key in standalone_keys: + layer_sd = grouped_state_dict[layer_key] + layers[f"{FLUX_LORA_TRANSFORMER_PREFIX}{layer_key}"] = any_lora_layer_from_state_dict(layer_sd) + + # Process merged layers. + for parent_key, sub_keys in merge_groups.items(): + # Sort by the numeric index at the end (e.g., qkv.0, qkv.1, qkv.2). + sub_keys.sort(key=lambda k: int(k.rsplit(".", maxsplit=1)[1])) + + sub_layers: list[BaseLayerPatch] = [] + sub_ranges: list[Range] = [] + dim_0_offset = 0 + + for sub_key in sub_keys: + layer_sd = grouped_state_dict[sub_key] + sub_layer = any_lora_layer_from_state_dict(layer_sd) + + # Determine the output dimension from the up weight shape. + up_weight = layer_sd["lora_up.weight"] + out_dim = up_weight.shape[0] + + sub_layers.append(sub_layer) + sub_ranges.append(Range(dim_0_offset, dim_0_offset + out_dim)) + dim_0_offset += out_dim + + layers[f"{FLUX_LORA_TRANSFORMER_PREFIX}{parent_key}"] = MergedLayerPatch(sub_layers, sub_ranges) + + return ModelPatchRaw(layers=layers) diff --git a/invokeai/backend/patches/lora_conversions/formats.py b/invokeai/backend/patches/lora_conversions/formats.py index 0b316602fc..b3e00c288b 100644 --- a/invokeai/backend/patches/lora_conversions/formats.py +++ b/invokeai/backend/patches/lora_conversions/formats.py @@ -14,6 +14,9 @@ from invokeai.backend.patches.lora_conversions.flux_diffusers_lora_conversion_ut from invokeai.backend.patches.lora_conversions.flux_kohya_lora_conversion_utils import ( is_state_dict_likely_in_flux_kohya_format, ) +from invokeai.backend.patches.lora_conversions.flux_onetrainer_bfl_lora_conversion_utils import ( + is_state_dict_likely_in_flux_onetrainer_bfl_format, +) from invokeai.backend.patches.lora_conversions.flux_onetrainer_lora_conversion_utils import ( is_state_dict_likely_in_flux_onetrainer_format, ) @@ -28,6 +31,8 @@ def flux_format_from_state_dict( ) -> FluxLoRAFormat | None: if is_state_dict_likely_in_flux_kohya_format(state_dict): return FluxLoRAFormat.Kohya + elif is_state_dict_likely_in_flux_onetrainer_bfl_format(state_dict, metadata): + return FluxLoRAFormat.OneTrainerBfl elif is_state_dict_likely_in_flux_onetrainer_format(state_dict): return FluxLoRAFormat.OneTrainer elif is_state_dict_likely_in_flux_diffusers_format(state_dict): From dbbf28925b7fb8ae06d5e89b407b86aa8dc6a5c4 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Tue, 7 Apr 2026 04:31:33 +0200 Subject: [PATCH 30/56] fix: detect FLUX.2 Klein 9B Base variant via filename heuristic (#9011) Klein 9B Base (undistilled) and Klein 9B (distilled) have identical architectures and cannot be distinguished from the state dict alone. Use a filename heuristic ("base" in the name) to detect the Base variant for checkpoint, GGUF, and diffusers format models. Also fixes the incorrect guidance_embeds-based detection for diffusers format, since both variants have guidance_embeds=False. --- .../backend/model_manager/configs/main.py | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/invokeai/backend/model_manager/configs/main.py b/invokeai/backend/model_manager/configs/main.py index 6f737ceb92..dff887f7d0 100644 --- a/invokeai/backend/model_manager/configs/main.py +++ b/invokeai/backend/model_manager/configs/main.py @@ -323,6 +323,16 @@ def _is_flux2_model(state_dict: dict[str | int, Any]) -> bool: return False +def _filename_suggests_base(name: str) -> bool: + """Check if a model name/filename suggests it is a Base (undistilled) variant. + + Klein 9B Base and Klein 9B have identical architectures and cannot be distinguished + from the state dict. We use the filename as a heuristic: filenames containing "base" + (e.g. "flux-2-klein-base-9b", "FLUX.2-klein-base-9B") indicate the undistilled model. + """ + return "base" in name.lower() + + def _get_flux2_variant(state_dict: dict[str | int, Any]) -> Flux2VariantType | None: """Determine FLUX.2 variant from state dict. @@ -330,9 +340,9 @@ def _get_flux2_variant(state_dict: dict[str | int, Any]) -> Flux2VariantType | N - Klein 4B: context_in_dim = 7680 (3 × Qwen3-4B hidden_size 2560) - Klein 9B: context_in_dim = 12288 (3 × Qwen3-8B hidden_size 4096) - Note: Klein 9B Base (undistilled) also has context_in_dim = 12288 but is rare. - We default to Klein9B (distilled) for all 9B models since GGUF models may not - include guidance embedding keys needed to distinguish them. + Note: Klein 9B (distilled) and Klein 9B Base (undistilled) have identical architectures + and cannot be distinguished from the state dict alone. This function defaults to Klein9B + for all 9B models. Callers should use filename heuristics to detect Klein9BBase. Supports both BFL format (checkpoint) and diffusers format keys: - BFL format: txt_in.weight (context embedder) @@ -366,7 +376,7 @@ def _get_flux2_variant(state_dict: dict[str | int, Any]) -> Flux2VariantType | N context_in_dim = shape[1] # Determine variant based on context dimension if context_in_dim == KLEIN_9B_CONTEXT_DIM: - # Default to Klein9B (distilled) - the official/common 9B model + # Default to Klein9B - callers use filename heuristics to detect Klein9BBase return Flux2VariantType.Klein9B elif context_in_dim == KLEIN_4B_CONTEXT_DIM: return Flux2VariantType.Klein4B @@ -553,6 +563,11 @@ class Main_Checkpoint_Flux2_Config(Checkpoint_Config_Base, Main_Config_Base, Con if variant is None: raise NotAMatchError("unable to determine FLUX.2 model variant from state dict") + # Klein 9B Base and Klein 9B have identical architectures. + # Use filename heuristic to detect the Base (undistilled) variant. + if variant == Flux2VariantType.Klein9B and _filename_suggests_base(mod.name): + return Flux2VariantType.Klein9BBase + return variant @classmethod @@ -720,6 +735,11 @@ class Main_GGUF_Flux2_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Ba if variant is None: raise NotAMatchError("unable to determine FLUX.2 model variant from state dict") + # Klein 9B Base and Klein 9B have identical architectures. + # Use filename heuristic to detect the Base (undistilled) variant. + if variant == Flux2VariantType.Klein9B and _filename_suggests_base(mod.name): + return Flux2VariantType.Klein9BBase + return variant @classmethod @@ -829,12 +849,8 @@ class Main_Diffusers_Flux2_Config(Diffusers_Config_Base, Main_Config_Base, Confi - Klein 4B: joint_attention_dim = 7680 (3×Qwen3-4B hidden size) - Klein 9B/9B Base: joint_attention_dim = 12288 (3×Qwen3-8B hidden size) - To distinguish Klein 9B (distilled) from Klein 9B Base (undistilled), - we check guidance_embeds: - - Klein 9B (distilled): guidance_embeds = False (guidance is "baked in" during distillation) - - Klein 9B Base (undistilled): guidance_embeds = True (needs guidance at inference) - - Note: The official BFL Klein 9B model is the distilled version with guidance_embeds=False. + Klein 9B (distilled) and Klein 9B Base (undistilled) have identical architectures + and both have guidance_embeds=False. We use a filename heuristic to detect Base models. """ KLEIN_4B_CONTEXT_DIM = 7680 # 3 × 2560 KLEIN_9B_CONTEXT_DIM = 12288 # 3 × 4096 @@ -842,17 +858,12 @@ class Main_Diffusers_Flux2_Config(Diffusers_Config_Base, Main_Config_Base, Confi transformer_config = get_config_dict_or_raise(mod.path / "transformer" / "config.json") joint_attention_dim = transformer_config.get("joint_attention_dim", 4096) - guidance_embeds = transformer_config.get("guidance_embeds", False) # Determine variant based on joint_attention_dim if joint_attention_dim == KLEIN_9B_CONTEXT_DIM: - # Check guidance_embeds to distinguish distilled from undistilled - # Klein 9B (distilled): guidance_embeds = False (guidance is baked in) - # Klein 9B Base (undistilled): guidance_embeds = True (needs guidance) - if guidance_embeds: + if _filename_suggests_base(mod.name): return Flux2VariantType.Klein9BBase - else: - return Flux2VariantType.Klein9B + return Flux2VariantType.Klein9B elif joint_attention_dim == KLEIN_4B_CONTEXT_DIM: return Flux2VariantType.Klein4B elif joint_attention_dim > 4096: From 80be1b72822d9c6df38f03adde38d85ab959b6a6 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Tue, 7 Apr 2026 05:09:29 +0200 Subject: [PATCH 31/56] fix: correct inaccurate download size estimates in starter models (#8968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified model sizes against Hugging Face repositories and corrected 11 descriptions that had wrong or outdated download size estimates. Key corrections: - T5-XXL base encoder: ~8GB → ~9.5GB - FLUX.2 VAE: ~335MB → ~168MB (was confused with FLUX.1 VAE) - FLUX.1 Krea dev: ~33GB → ~29GB (uses quantized T5, not full) - FLUX.2 Klein 4B/9B Diffusers: ~10GB/~20GB → ~16GB/~35GB - SD3.5 Medium/Large: ~15GB/~19G → ~16GB/~28GB - CogView4: ~29GB → ~31GB - Z-Image Turbo: ~30.6GB → ~33GB - FLUX.1 Kontext/Krea quantized: ~14GB → ~12GB --- .../backend/model_manager/starter_models.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py index 9f86f83dc5..3f09ddbe76 100644 --- a/invokeai/backend/model_manager/starter_models.py +++ b/invokeai/backend/model_manager/starter_models.py @@ -71,7 +71,7 @@ t5_base_encoder = StarterModel( name="t5_base_encoder", base=BaseModelType.Any, source="InvokeAI/t5-v1_1-xxl::bfloat16", - description="T5-XXL text encoder (used in FLUX pipelines). ~8GB", + description="T5-XXL text encoder (used in FLUX pipelines). ~9.5GB", type=ModelType.T5Encoder, ) @@ -156,7 +156,7 @@ flux_kontext_quantized = StarterModel( name="FLUX.1 Kontext dev (quantized)", base=BaseModelType.Flux, source="https://huggingface.co/unsloth/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_M.gguf", - description="FLUX.1 Kontext dev quantized (q4_k_m). Total size with dependencies: ~14GB", + description="FLUX.1 Kontext dev quantized (q4_k_m). Total size with dependencies: ~12GB", type=ModelType.Main, dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder], ) @@ -164,7 +164,7 @@ flux_krea = StarterModel( name="FLUX.1 Krea dev", base=BaseModelType.Flux, source="https://huggingface.co/InvokeAI/FLUX.1-Krea-dev/resolve/main/flux1-krea-dev.safetensors", - description="FLUX.1 Krea dev. Total size with dependencies: ~33GB", + description="FLUX.1 Krea dev. Total size with dependencies: ~29GB", type=ModelType.Main, dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder], ) @@ -172,7 +172,7 @@ flux_krea_quantized = StarterModel( name="FLUX.1 Krea dev (quantized)", base=BaseModelType.Flux, source="https://huggingface.co/InvokeAI/FLUX.1-Krea-dev-GGUF/resolve/main/flux1-krea-dev-Q4_K_M.gguf", - description="FLUX.1 Krea dev quantized (q4_k_m). Total size with dependencies: ~14GB", + description="FLUX.1 Krea dev quantized (q4_k_m). Total size with dependencies: ~12GB", type=ModelType.Main, dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder], ) @@ -180,7 +180,7 @@ sd35_medium = StarterModel( name="SD3.5 Medium", base=BaseModelType.StableDiffusion3, source="stabilityai/stable-diffusion-3.5-medium", - description="Medium SD3.5 Model: ~15GB", + description="Medium SD3.5 Model: ~16GB", type=ModelType.Main, dependencies=[], ) @@ -188,7 +188,7 @@ sd35_large = StarterModel( name="SD3.5 Large", base=BaseModelType.StableDiffusion3, source="stabilityai/stable-diffusion-3.5-large", - description="Large SD3.5 Model: ~19G", + description="Large SD3.5 Model: ~28GB", type=ModelType.Main, dependencies=[], ) @@ -644,7 +644,7 @@ cogview4 = StarterModel( name="CogView4", base=BaseModelType.CogView4, source="THUDM/CogView4-6B", - description="The base CogView4 model (~29GB).", + description="The base CogView4 model (~31GB).", type=ModelType.Main, ) # endregion @@ -695,7 +695,7 @@ flux2_vae = StarterModel( name="FLUX.2 VAE", base=BaseModelType.Flux2, source="black-forest-labs/FLUX.2-klein-4B::vae", - description="FLUX.2 VAE (16-channel, same architecture as FLUX.1 VAE). ~335MB", + description="FLUX.2 VAE (16-channel, same architecture as FLUX.1 VAE). ~168MB", type=ModelType.VAE, ) @@ -719,7 +719,7 @@ flux2_klein_4b = StarterModel( name="FLUX.2 Klein 4B (Diffusers)", base=BaseModelType.Flux2, source="black-forest-labs/FLUX.2-klein-4B", - description="FLUX.2 Klein 4B in Diffusers format - includes transformer, VAE and Qwen3 encoder. ~10GB", + description="FLUX.2 Klein 4B in Diffusers format - includes transformer, VAE and Qwen3 encoder. ~16GB", type=ModelType.Main, ) @@ -745,7 +745,7 @@ flux2_klein_9b = StarterModel( name="FLUX.2 Klein 9B (Diffusers)", base=BaseModelType.Flux2, source="black-forest-labs/FLUX.2-klein-9B", - description="FLUX.2 Klein 9B in Diffusers format - includes transformer, VAE and Qwen3 encoder. ~20GB", + description="FLUX.2 Klein 9B in Diffusers format - includes transformer, VAE and Qwen3 encoder. ~35GB", type=ModelType.Main, ) @@ -821,7 +821,7 @@ z_image_turbo = StarterModel( name="Z-Image Turbo", base=BaseModelType.ZImage, source="Tongyi-MAI/Z-Image-Turbo", - description="Z-Image Turbo - fast 6B parameter text-to-image model with 8 inference steps. Supports bilingual prompts (English & Chinese). ~30.6GB", + description="Z-Image Turbo - fast 6B parameter text-to-image model with 8 inference steps. Supports bilingual prompts (English & Chinese). ~33GB", type=ModelType.Main, ) From 60d0bcdbc1755dad460eb7123082eb51093bfea8 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Tue, 7 Apr 2026 05:25:06 +0200 Subject: [PATCH 32/56] Feature(UI): Canvas Workflow Integration - Run Workflow on Raster Layer (#8665) * feat: Add canvas-workflow integration feature This commit implements a new feature that allows users to run workflows directly from the unified canvas. Users can now: - Access a "Run Workflow" option from the canvas layer context menu - Select a workflow with image parameters from a modal dialog - Customize workflow parameters (non-image fields) - Execute the workflow with the current canvas layer as input - Have the result automatically added back to the canvas Key changes: - Added canvasWorkflowIntegrationSlice for state management - Created CanvasWorkflowIntegrationModal and related UI components - Added context menu item to raster layers - Integrated workflow execution with canvas image extraction - Added modal to global modal isolator This integration enhances the canvas by allowing users to leverage custom workflows for advanced image processing directly within the canvas workspace. Implements feature request for deeper workflow-canvas integration. * refactor(ui): simplify canvas workflow integration field rendering - Extract WorkflowFieldRenderer component for individual field rendering - Add WorkflowFormPreview component to handle workflow parameter display - Remove workflow compatibility filtering - allow all workflows - Simplify workflow selector to use flattened workflow list - Add comprehensive field type support (String, Integer, Float, Boolean, Enum, Scheduler, Board, Model, Image, Color) - Implement image field selection UI with radio * feat(ui): add canvas-workflow-integration logging namespace * feat(ui): add workflow filtering for canvas-workflow integration - Add useFilteredWorkflows hook to filter workflows with ImageField inputs - Add workflowHasImageField utility to check for ImageField in Form Builder - Only show workflows that have Form Builder with at least one ImageField - Add loading state while filtering workflows - Improve error messages to clarify Form Builder requirement - Update modal description to mention Form Builder and parameter adjustment - Add fallback error message for workflows without Form Builder * feat(ui): add persistence and migration for canvas workflow integration state - Add _version field (v1) to canvasWorkflowIntegrationState for future migrations - Add persistConfig with migration function to handle version upgrades - Add persistDenylist to exclude transient state (isOpen, isProcessing, sourceEntityIdentifier) - Use es-toolkit isPlainObject and tsafe assert for type-safe migration - Persist selectedWorkflowId and fieldValues across sessions * pnpm fix imports * fix(ui): handle workflow errors in canvas staging area and improve form UX - Clear processing state when workflow execution fails at enqueue time or during invocation, so the modal doesn't get stuck - Optimistically update listAllQueueItems cache on queue item status changes so the staging area immediately exits on failure - Clear processing state on invocation_error for canvas workflow origin - Auto-select the only unfilled ImageField in workflow form - Fix image field overflow and thumbnail sizing in workflow form * feat(ui): add canvas_output node and entry-based staging area Add a dedicated `canvas_output` backend invocation node that explicitly marks which images go to the canvas staging area, replacing the fragile board-based heuristic. Each `canvas_output` node produces a separate navigable entry in the staging area, allowing workflows with multiple outputs to be individually previewed and accepted. Key changes: - New `CanvasOutputInvocation` backend node (canvas.py) - Entry-based staging area model where each output image is a separate navigable entry with flat next/prev cycling across all items - Frontend execute hook uses `canvas_output` type detection instead of board field heuristic, with proper board field value translation - Workflow filtering requires both Form Builder and canvas_output node - Updated QueueItemPreviewMini and StagingAreaItemsList for entries - Tests for entry-based navigation, multi-output, and race conditions * Chore pnp run fix * Chore eslint fix * Remove unused useOutputImageDTO export to fix knip lint * Update invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/useCanvasWorkflowIntegrationExecute.tsx Co-authored-by: dunkeroni * move UI text to en.json * fix conflicts merge with main * generate schema * Chore typegen --------- Co-authored-by: Claude Co-authored-by: Lincoln Stein Co-authored-by: dunkeroni --- invokeai/app/invocations/canvas.py | 27 + invokeai/frontend/web/public/locales/en.json | 21 + .../app/components/GlobalModalIsolator.tsx | 2 + .../frontend/web/src/app/logging/logger.ts | 1 + invokeai/frontend/web/src/app/store/store.ts | 3 + .../CanvasWorkflowIntegrationModal.tsx | 93 +++ ...anvasWorkflowIntegrationParameterPanel.tsx | 13 + ...vasWorkflowIntegrationWorkflowSelector.tsx | 92 +++ .../WorkflowFieldRenderer.tsx | 548 ++++++++++++++++++ .../WorkflowFormPreview.tsx | 289 +++++++++ .../useCanvasWorkflowIntegrationExecute.tsx | 302 ++++++++++ .../useFilteredWorkflows.tsx | 107 ++++ .../workflowHasImageField.tsx | 86 +++ .../RasterLayer/RasterLayerMenuItems.tsx | 2 + .../StagingArea/QueueItemPreviewMini.tsx | 36 +- .../StagingArea/StagingAreaItemsList.tsx | 20 +- .../components/StagingArea/context.tsx | 11 +- .../components/StagingArea/shared.test.ts | 38 +- .../components/StagingArea/shared.ts | 29 +- .../components/StagingArea/state.test.ts | 427 +++++++++++++- .../components/StagingArea/state.ts | 257 +++++--- .../CanvasEntityMenuItemsRunWorkflow.tsx | 25 + .../konva/CanvasStagingAreaModule.ts | 4 +- .../store/canvasWorkflowIntegrationSlice.ts | 134 +++++ .../nodes/util/graph/graphBuilderUtils.ts | 4 +- .../frontend/web/src/services/api/schema.ts | 49 +- .../services/events/onInvocationComplete.tsx | 28 + .../src/services/events/setEventListeners.tsx | 24 + 28 files changed, 2510 insertions(+), 162 deletions(-) create mode 100644 invokeai/app/invocations/canvas.py create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationParameterPanel.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationWorkflowSelector.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFieldRenderer.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFormPreview.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/useCanvasWorkflowIntegrationExecute.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/useFilteredWorkflows.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/workflowHasImageField.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsRunWorkflow.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/canvasWorkflowIntegrationSlice.ts diff --git a/invokeai/app/invocations/canvas.py b/invokeai/app/invocations/canvas.py new file mode 100644 index 0000000000..cf13c3334f --- /dev/null +++ b/invokeai/app/invocations/canvas.py @@ -0,0 +1,27 @@ +from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation +from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField +from invokeai.app.invocations.primitives import ImageOutput +from invokeai.app.services.shared.invocation_context import InvocationContext + + +@invocation( + "canvas_output", + title="Canvas Output", + tags=["canvas", "output", "image"], + category="canvas", + version="1.0.0", + use_cache=False, +) +class CanvasOutputInvocation(BaseInvocation): + """Outputs an image to the canvas staging area. + + Use this node in workflows intended for canvas workflow integration. + Connect the final image of your workflow to this node to send it + to the canvas staging area when run via 'Run Workflow on Canvas'.""" + + image: ImageField = InputField(description=FieldDescriptions.image) + + def invoke(self, context: InvocationContext) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) + image_dto = context.images.save(image=image) + return ImageOutput.build(image_dto) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 047d5a4007..9ba645eef8 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2377,6 +2377,27 @@ "pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage", "addAdjustments": "Add Adjustments", "removeAdjustments": "Remove Adjustments", + "workflowIntegration": { + "title": "Run Workflow on Canvas", + "description": "Select a workflow with a Canvas Output node and an image parameter to run on the current canvas layer. You can adjust parameters before executing. The result will be added back to the canvas.", + "execute": "Execute Workflow", + "executing": "Executing...", + "runWorkflow": "Run Workflow", + "filteringWorkflows": "Filtering workflows...", + "loadingWorkflows": "Loading workflows...", + "noWorkflowsFound": "No workflows found.", + "noWorkflowsWithImageField": "No compatible workflows found. A workflow needs a Form Builder with an image input field and a Canvas Output node.", + "selectWorkflow": "Select Workflow", + "selectPlaceholder": "Choose a workflow...", + "unnamedWorkflow": "Unnamed Workflow", + "loadingParameters": "Loading workflow parameters...", + "noFormBuilderError": "This workflow has no form builder and cannot be used. Please select a different workflow.", + "imageFieldSelected": "This field will receive the canvas image", + "imageFieldNotSelected": "Click to use this field for canvas image", + "executionStarted": "Workflow execution started", + "executionStartedDescription": "The result will appear in the staging area when complete.", + "executionFailed": "Failed to execute workflow" + }, "compositeOperation": { "label": "Blend Mode", "add": "Add Blend Mode", diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx index 5c1446662e..ef0747707f 100644 --- a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx @@ -1,6 +1,7 @@ import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys'; import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal'; import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal'; +import { CanvasWorkflowIntegrationModal } from 'features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { CropImageModal } from 'features/cropper/components/CropImageModal'; import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal'; @@ -51,6 +52,7 @@ export const GlobalModalIsolator = memo(() => { + diff --git a/invokeai/frontend/web/src/app/logging/logger.ts b/invokeai/frontend/web/src/app/logging/logger.ts index 6c843068df..d20ef77090 100644 --- a/invokeai/frontend/web/src/app/logging/logger.ts +++ b/invokeai/frontend/web/src/app/logging/logger.ts @@ -16,6 +16,7 @@ const $logger = atom(Roarr.child(BASE_CONTEXT)); export const zLogNamespace = z.enum([ 'canvas', + 'canvas-workflow-integration', 'config', 'dnd', 'events', diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 8f077baaea..f24d2d0105 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -25,6 +25,7 @@ import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSe import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice'; import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { canvasTextSliceConfig } from 'features/controlLayers/store/canvasTextSlice'; +import { canvasWorkflowIntegrationSliceConfig } from 'features/controlLayers/store/canvasWorkflowIntegrationSlice'; import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice'; import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice'; @@ -67,6 +68,7 @@ const SLICE_CONFIGS = { [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig, [canvasTextSliceConfig.slice.reducerPath]: canvasTextSliceConfig, [canvasSliceConfig.slice.reducerPath]: canvasSliceConfig, + [canvasWorkflowIntegrationSliceConfig.slice.reducerPath]: canvasWorkflowIntegrationSliceConfig, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig, [dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig, [gallerySliceConfig.slice.reducerPath]: gallerySliceConfig, @@ -98,6 +100,7 @@ const ALL_REDUCERS = { canvasSliceConfig.slice.reducer, canvasSliceConfig.undoableConfig?.reduxUndoOptions ), + [canvasWorkflowIntegrationSliceConfig.slice.reducerPath]: canvasWorkflowIntegrationSliceConfig.slice.reducer, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer, [dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer, [gallerySliceConfig.slice.reducerPath]: gallerySliceConfig.slice.reducer, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal.tsx new file mode 100644 index 0000000000..94a123fa91 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal.tsx @@ -0,0 +1,93 @@ +import { + Button, + ButtonGroup, + Flex, + Heading, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Spacer, + Spinner, + Text, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + canvasWorkflowIntegrationClosed, + selectCanvasWorkflowIntegrationIsOpen, + selectCanvasWorkflowIntegrationIsProcessing, + selectCanvasWorkflowIntegrationSelectedWorkflowId, +} from 'features/controlLayers/store/canvasWorkflowIntegrationSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { CanvasWorkflowIntegrationParameterPanel } from './CanvasWorkflowIntegrationParameterPanel'; +import { CanvasWorkflowIntegrationWorkflowSelector } from './CanvasWorkflowIntegrationWorkflowSelector'; +import { useCanvasWorkflowIntegrationExecute } from './useCanvasWorkflowIntegrationExecute'; + +export const CanvasWorkflowIntegrationModal = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const isOpen = useAppSelector(selectCanvasWorkflowIntegrationIsOpen); + const isProcessing = useAppSelector(selectCanvasWorkflowIntegrationIsProcessing); + const selectedWorkflowId = useAppSelector(selectCanvasWorkflowIntegrationSelectedWorkflowId); + + const { execute, canExecute } = useCanvasWorkflowIntegrationExecute(); + + const onClose = useCallback(() => { + if (!isProcessing) { + dispatch(canvasWorkflowIntegrationClosed()); + } + }, [dispatch, isProcessing]); + + const onExecute = useCallback(() => { + execute(); + }, [execute]); + + return ( + + + + + {t('controlLayers.workflowIntegration.title')} + + + + + + + {t('controlLayers.workflowIntegration.description')} + + + + + {selectedWorkflowId && } + + + + + + + + + + + + + ); +}); + +CanvasWorkflowIntegrationModal.displayName = 'CanvasWorkflowIntegrationModal'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationParameterPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationParameterPanel.tsx new file mode 100644 index 0000000000..f59a6c45ed --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationParameterPanel.tsx @@ -0,0 +1,13 @@ +import { Box } from '@invoke-ai/ui-library'; +import { WorkflowFormPreview } from 'features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFormPreview'; +import { memo } from 'react'; + +export const CanvasWorkflowIntegrationParameterPanel = memo(() => { + return ( + + + + ); +}); + +CanvasWorkflowIntegrationParameterPanel.displayName = 'CanvasWorkflowIntegrationParameterPanel'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationWorkflowSelector.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationWorkflowSelector.tsx new file mode 100644 index 0000000000..30bc60605c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationWorkflowSelector.tsx @@ -0,0 +1,92 @@ +import { Flex, FormControl, FormLabel, Select, Spinner, Text } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + canvasWorkflowIntegrationWorkflowSelected, + selectCanvasWorkflowIntegrationSelectedWorkflowId, +} from 'features/controlLayers/store/canvasWorkflowIntegrationSlice'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useListWorkflowsInfiniteInfiniteQuery } from 'services/api/endpoints/workflows'; + +import { useFilteredWorkflows } from './useFilteredWorkflows'; + +export const CanvasWorkflowIntegrationWorkflowSelector = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const selectedWorkflowId = useAppSelector(selectCanvasWorkflowIntegrationSelectedWorkflowId); + const { data: workflowsData, isLoading } = useListWorkflowsInfiniteInfiniteQuery( + { + per_page: 100, // Get a reasonable number of workflows + page: 0, + }, + { + selectFromResult: ({ data, isLoading }) => ({ + data, + isLoading, + }), + } + ); + + const workflows = useMemo(() => { + if (!workflowsData) { + return []; + } + // Flatten all pages into a single list + return workflowsData.pages.flatMap((page) => page.items); + }, [workflowsData]); + + // Filter workflows to only show those with ImageFields + const { filteredWorkflows, isFiltering } = useFilteredWorkflows(workflows); + + const onChange = useCallback( + (e: ChangeEvent) => { + const workflowId = e.target.value || null; + dispatch(canvasWorkflowIntegrationWorkflowSelected({ workflowId })); + }, + [dispatch] + ); + + if (isLoading || isFiltering) { + return ( + + + + {isFiltering + ? t('controlLayers.workflowIntegration.filteringWorkflows') + : t('controlLayers.workflowIntegration.loadingWorkflows')} + + + ); + } + + if (filteredWorkflows.length === 0) { + return ( + + {workflows.length === 0 + ? t('controlLayers.workflowIntegration.noWorkflowsFound') + : t('controlLayers.workflowIntegration.noWorkflowsWithImageField')} + + ); + } + + return ( + + {t('controlLayers.workflowIntegration.selectWorkflow')} + + + ); +}); + +CanvasWorkflowIntegrationWorkflowSelector.displayName = 'CanvasWorkflowIntegrationWorkflowSelector'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFieldRenderer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFieldRenderer.tsx new file mode 100644 index 0000000000..2d91be13bf --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFieldRenderer.tsx @@ -0,0 +1,548 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { + Combobox, + Flex, + FormControl, + FormLabel, + IconButton, + Input, + Radio, + Select, + Switch, + Text, + Textarea, +} from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { logger } from 'app/logging/logger'; +import { EMPTY_ARRAY } from 'app/store/constants'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { UploadImageIconButton } from 'common/hooks/useImageUploadButton'; +import { + canvasWorkflowIntegrationFieldValueChanged, + canvasWorkflowIntegrationImageFieldSelected, + selectCanvasWorkflowIntegrationFieldValues, + selectCanvasWorkflowIntegrationSelectedImageFieldKey, + selectCanvasWorkflowIntegrationSelectedWorkflowId, +} from 'features/controlLayers/store/canvasWorkflowIntegrationSlice'; +import { DndImage } from 'features/dnd/DndImage'; +import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import type { NodeFieldElement } from 'features/nodes/types/workflow'; +import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; +import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleBold } from 'react-icons/pi'; +import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models'; +import { useGetWorkflowQuery } from 'services/api/endpoints/workflows'; +import type { AnyModelConfig, ImageDTO } from 'services/api/types'; + +const log = logger('canvas-workflow-integration'); + +interface WorkflowFieldRendererProps { + el: NodeFieldElement; +} + +export const WorkflowFieldRenderer = memo(({ el }: WorkflowFieldRendererProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const selectedWorkflowId = useAppSelector(selectCanvasWorkflowIntegrationSelectedWorkflowId); + const fieldValues = useAppSelector(selectCanvasWorkflowIntegrationFieldValues); + const selectedImageFieldKey = useAppSelector(selectCanvasWorkflowIntegrationSelectedImageFieldKey); + const templates = useStore($templates); + + const { data: workflow } = useGetWorkflowQuery(selectedWorkflowId!, { + skip: !selectedWorkflowId, + }); + + // Load boards and models for BoardField and ModelIdentifierField + const { data: boardsData } = useListAllBoardsQuery({ include_archived: true }); + const { data: modelsData, isLoading: isLoadingModels } = useGetModelConfigsQuery(); + + const { fieldIdentifier } = el.data; + const fieldKey = `${fieldIdentifier.nodeId}.${fieldIdentifier.fieldName}`; + + log.debug({ fieldIdentifier, fieldKey }, 'Rendering workflow field'); + + // Get the node, field instance, and field template + const { field, fieldTemplate } = useMemo(() => { + if (!workflow?.workflow.nodes) { + log.warn('No workflow nodes found'); + return { field: null, fieldTemplate: null }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const foundNode = workflow.workflow.nodes.find((n: any) => n.data.id === fieldIdentifier.nodeId); + if (!foundNode) { + log.warn({ nodeId: fieldIdentifier.nodeId }, 'Node not found'); + return { field: null, fieldTemplate: null }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const foundField = (foundNode.data as any).inputs[fieldIdentifier.fieldName]; + if (!foundField) { + log.warn({ nodeId: fieldIdentifier.nodeId, fieldName: fieldIdentifier.fieldName }, 'Field not found in node'); + return { field: null, fieldTemplate: null }; + } + + // Get the field template from the invocation templates + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodeType = (foundNode.data as any).type; + const template = templates[nodeType]; + if (!template) { + log.warn({ nodeType }, 'No template found for node type'); + return { field: foundField, fieldTemplate: null }; + } + + const foundFieldTemplate = template.inputs[fieldIdentifier.fieldName]; + if (!foundFieldTemplate) { + log.warn({ nodeType, fieldName: fieldIdentifier.fieldName }, 'Field template not found'); + return { field: foundField, fieldTemplate: null }; + } + + return { field: foundField, fieldTemplate: foundFieldTemplate }; + }, [workflow, fieldIdentifier, templates]); + + // Get the current value from Redux or fallback to field default + const currentValue = useMemo(() => { + if (fieldValues && fieldKey in fieldValues) { + return fieldValues[fieldKey]; + } + + return field?.value ?? fieldTemplate?.default ?? ''; + }, [fieldValues, fieldKey, field, fieldTemplate]); + + // Get field type from the template + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fieldType = fieldTemplate ? (fieldTemplate as any).type?.name : null; + + const handleChange = useCallback( + (value: unknown) => { + dispatch(canvasWorkflowIntegrationFieldValueChanged({ fieldName: fieldKey, value })); + }, + [dispatch, fieldKey] + ); + + const handleStringChange = useCallback( + (e: ChangeEvent) => { + handleChange(e.target.value); + }, + [handleChange] + ); + + const handleNumberChange = useCallback( + (e: ChangeEvent) => { + const val = fieldType === 'IntegerField' ? parseInt(e.target.value, 10) : parseFloat(e.target.value); + handleChange(isNaN(val) ? 0 : val); + }, + [handleChange, fieldType] + ); + + const handleBooleanChange = useCallback( + (e: ChangeEvent) => { + handleChange(e.target.checked); + }, + [handleChange] + ); + + const handleSelectChange = useCallback( + (e: ChangeEvent) => { + handleChange(e.target.value); + }, + [handleChange] + ); + + // SchedulerField handlers + const handleSchedulerChange = useCallback( + (v) => { + if (!isParameterScheduler(v?.value)) { + return; + } + handleChange(v.value); + }, + [handleChange] + ); + + const schedulerValue = useMemo(() => SCHEDULER_OPTIONS.find((o) => o.value === currentValue), [currentValue]); + + // BoardField handlers + const handleBoardChange = useCallback( + (v) => { + if (!v) { + return; + } + const value = v.value === 'auto' || v.value === 'none' ? v.value : { board_id: v.value }; + handleChange(value); + }, + [handleChange] + ); + + const boardOptions = useMemo(() => { + const _options: ComboboxOption[] = [ + { label: t('common.auto'), value: 'auto' }, + { label: `${t('common.none')} (${t('boards.uncategorized')})`, value: 'none' }, + ]; + if (boardsData) { + for (const board of boardsData) { + _options.push({ + label: board.board_name, + value: board.board_id, + }); + } + } + return _options; + }, [boardsData, t]); + + const boardValue = useMemo(() => { + const _value = currentValue; + const autoOption = boardOptions[0]; + const noneOption = boardOptions[1]; + if (!_value || _value === 'auto') { + return autoOption; + } + if (_value === 'none') { + return noneOption; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const boardId = typeof _value === 'object' ? (_value as any).board_id : _value; + const boardOption = boardOptions.find((o) => o.value === boardId); + return boardOption ?? autoOption; + }, [currentValue, boardOptions]); + + const noOptionsMessage = useCallback(() => t('boards.noMatching'), [t]); + + // ModelIdentifierField handlers + const handleModelChange = useCallback( + (value: AnyModelConfig | null) => { + if (!value) { + return; + } + handleChange(value); + }, + [handleChange] + ); + + const modelConfigs = useMemo(() => { + if (!modelsData) { + return EMPTY_ARRAY; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ui_model_base = fieldTemplate ? (fieldTemplate as any)?.ui_model_base : null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ui_model_type = fieldTemplate ? (fieldTemplate as any)?.ui_model_type : null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ui_model_variant = fieldTemplate ? (fieldTemplate as any)?.ui_model_variant : null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ui_model_format = fieldTemplate ? (fieldTemplate as any)?.ui_model_format : null; + + if (!ui_model_base && !ui_model_type) { + return modelConfigsAdapterSelectors.selectAll(modelsData); + } + + return modelConfigsAdapterSelectors.selectAll(modelsData).filter((config) => { + if (ui_model_base && !ui_model_base.includes(config.base)) { + return false; + } + if (ui_model_type && !ui_model_type.includes(config.type)) { + return false; + } + if (ui_model_variant && 'variant' in config && config.variant && !ui_model_variant.includes(config.variant)) { + return false; + } + if (ui_model_format && !ui_model_format.includes(config.format)) { + return false; + } + return true; + }); + }, [modelsData, fieldTemplate]); + + // ImageField handler + const handleImageFieldSelect = useCallback(() => { + dispatch(canvasWorkflowIntegrationImageFieldSelected({ fieldKey })); + }, [dispatch, fieldKey]); + + if (!field || !fieldTemplate) { + log.warn({ fieldIdentifier }, 'Field or template is null - not rendering'); + return null; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const label = (field as any)?.label || (fieldTemplate as any)?.title || fieldIdentifier.fieldName; + + // Log the entire field structure to understand its shape + log.debug( + { fieldType, label, currentValue, fieldStructure: field, fieldTemplateStructure: fieldTemplate }, + 'Field info' + ); + + // ImageField - allow user to select which one receives the canvas image + if (fieldType === 'ImageField') { + return ( + + ); + } + + // Render different input types based on field type + if (fieldType === 'StringField') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const isTextarea = (fieldTemplate as any)?.ui_component === 'textarea'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const isRequired = (fieldTemplate as any)?.required ?? false; + + if (isTextarea) { + return ( + + {label} +