Merge branch 'main' into llava

This commit is contained in:
jazzhaiku
2025-03-24 15:17:33 +11:00
committed by GitHub
73 changed files with 1571 additions and 547 deletions

View File

@@ -27,7 +27,7 @@ class FluxFillOutput(BaseInvocationOutput):
@invocation(
"flux_fill",
title="FLUX Fill",
title="FLUX Fill Conditioning",
tags=["inpaint"],
category="inpaint",
version="1.0.0",

View File

@@ -28,10 +28,10 @@ class FluxLoRALoaderOutput(BaseInvocationOutput):
@invocation(
"flux_lora_loader",
title="FLUX LoRA",
title="Apply LoRA - FLUX",
tags=["lora", "model", "flux"],
category="model",
version="1.2.0",
version="1.2.1",
classification=Classification.Prototype,
)
class FluxLoRALoaderInvocation(BaseInvocation):
@@ -107,10 +107,10 @@ class FluxLoRALoaderInvocation(BaseInvocation):
@invocation(
"flux_lora_collection_loader",
title="FLUX LoRA Collection Loader",
title="Apply LoRA Collection - FLUX",
tags=["lora", "model", "flux"],
category="model",
version="1.3.0",
version="1.3.1",
classification=Classification.Prototype,
)
class FLUXLoRACollectionLoader(BaseInvocation):

View File

@@ -1051,7 +1051,7 @@ class MaskFromIDInvocation(BaseInvocation, WithMetadata, WithBoard):
tags=["image", "mask", "id"],
category="image",
version="1.0.0",
classification=Classification.Internal,
classification=Classification.Deprecated,
)
class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Handles Canvas V2 image output masking and cropping"""
@@ -1089,6 +1089,112 @@ class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard):
return ImageOutput.build(image_dto)
@invocation(
"expand_mask_with_fade", title="Expand Mask with Fade", tags=["image", "mask"], category="image", version="1.0.0"
)
class ExpandMaskWithFadeInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Expands a mask with a fade effect. The mask uses black to indicate areas to keep from the generated image and white for areas to discard.
The mask is thresholded to create a binary mask, and then a distance transform is applied to create a fade effect.
The fade size is specified in pixels, and the mask is expanded by that amount. The result is a mask with a smooth transition from black to white.
"""
mask: ImageField = InputField(description="The mask to expand")
threshold: int = InputField(default=0, ge=0, le=255, description="The threshold for the binary mask (0-255)")
fade_size_px: int = InputField(default=32, ge=0, description="The size of the fade in pixels")
def invoke(self, context: InvocationContext) -> ImageOutput:
pil_mask = context.images.get_pil(self.mask.image_name, mode="L")
np_mask = numpy.array(pil_mask)
# Threshold the mask to create a binary mask - 0 for black, 255 for white
# If we don't threshold we can get some weird artifacts
np_mask = numpy.where(np_mask > self.threshold, 255, 0).astype(numpy.uint8)
# Create a mask for the black region (1 where black, 0 otherwise)
black_mask = (np_mask == 0).astype(numpy.uint8)
# Invert the black region
bg_mask = 1 - black_mask
# Create a distance transform of the inverted mask
dist = cv2.distanceTransform(bg_mask, cv2.DIST_L2, 5)
# Normalize distances so that pixels <fade_size_px become a linear gradient (0 to 1)
d_norm = numpy.clip(dist / self.fade_size_px, 0, 1)
# Control points: x values (normalized distance) and corresponding fade pct y values.
# There are some magic numbers here that are used to create a smooth transition:
# - The first point is at 0% of fade size from edge of mask (meaning the edge of the mask), and is 0% fade (black)
# - The second point is 1px from the edge of the mask and also has 0% fade, effectively expanding the mask
# by 1px. This fixes an issue where artifacts can occur at the edge of the mask
# - The third point is at 20% of the fade size from the edge of the mask and has 20% fade
# - The fourth point is at 80% of the fade size from the edge of the mask and has 90% fade
# - The last point is at 100% of the fade size from the edge of the mask and has 100% fade (white)
# x values: 0 = mask edge, 1 = fade_size_px from edge
x_control = numpy.array([0.0, 1.0 / self.fade_size_px, 0.2, 0.8, 1.0])
# y values: 0 = black, 1 = white
y_control = numpy.array([0.0, 0.0, 0.2, 0.9, 1.0])
# Fit a cubic polynomial that smoothly passes through the control points
coeffs = numpy.polyfit(x_control, y_control, 3)
poly = numpy.poly1d(coeffs)
# Evaluate and clip the smooth mapping
feather = numpy.clip(poly(d_norm), 0, 1)
# Build final image.
np_result = numpy.where(black_mask == 1, 0, (feather * 255).astype(numpy.uint8))
# Convert back to PIL, grayscale
pil_result = Image.fromarray(np_result.astype(numpy.uint8), mode="L")
image_dto = context.images.save(image=pil_result, image_category=ImageCategory.MASK)
return ImageOutput.build(image_dto)
@invocation(
"apply_mask_to_image",
title="Apply Mask to Image",
tags=["image", "mask", "blend"],
category="image",
version="1.0.0",
)
class ApplyMaskToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
"""
Extracts a region from a generated image using a mask and blends it seamlessly onto a source image.
The mask uses black to indicate areas to keep from the generated image and white for areas to discard.
"""
image: ImageField = InputField(description="The image from which to extract the masked region")
mask: ImageField = InputField(description="The mask defining the region (black=keep, white=discard)")
invert_mask: bool = InputField(
default=False,
description="Whether to invert the mask before applying it",
)
def invoke(self, context: InvocationContext) -> ImageOutput:
# Load images
image = context.images.get_pil(self.image.image_name, mode="RGBA")
mask = context.images.get_pil(self.mask.image_name, mode="L")
if self.invert_mask:
# Invert the mask if requested
mask = ImageOps.invert(mask.copy())
# Combine the mask as the alpha channel of the image
r, g, b, _ = image.split() # Split the image into RGB and alpha channels
result_image = Image.merge("RGBA", (r, g, b, mask)) # Use the mask as the new alpha channel
# Save the resulting image
image_dto = context.images.save(image=result_image)
return ImageOutput.build(image_dto)
@invocation(
"img_noise",
title="Add Image Noise",

View File

@@ -67,7 +67,7 @@ class AlphaMaskToTensorInvocation(BaseInvocation):
invert: bool = InputField(default=False, description="Whether to invert the mask.")
def invoke(self, context: InvocationContext) -> MaskOutput:
image = context.images.get_pil(self.image.image_name)
image = context.images.get_pil(self.image.image_name, mode="RGBA")
mask = torch.zeros((1, image.height, image.width), dtype=torch.bool)
if self.invert:
mask[0] = torch.tensor(np.array(image)[:, :, 3] == 0, dtype=torch.bool)

View File

@@ -181,7 +181,7 @@ class LoRALoaderOutput(BaseInvocationOutput):
clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP")
@invocation("lora_loader", title="LoRA", tags=["model"], category="model", version="1.0.3")
@invocation("lora_loader", title="Apply LoRA - SD1.5", tags=["model"], category="model", version="1.0.4")
class LoRALoaderInvocation(BaseInvocation):
"""Apply selected lora to unet and text_encoder."""
@@ -244,7 +244,7 @@ class LoRASelectorOutput(BaseInvocationOutput):
lora: LoRAField = OutputField(description="LoRA model and weight", title="LoRA")
@invocation("lora_selector", title="LoRA Model - SD1.5", tags=["model"], category="model", version="1.0.2")
@invocation("lora_selector", title="Select LoRA", tags=["model"], category="model", version="1.0.3")
class LoRASelectorInvocation(BaseInvocation):
"""Selects a LoRA model and weight."""
@@ -258,7 +258,7 @@ class LoRASelectorInvocation(BaseInvocation):
@invocation(
"lora_collection_loader", title="LoRA Collection - SD1.5", tags=["model"], category="model", version="1.1.1"
"lora_collection_loader", title="Apply LoRA Collection - SD1.5", tags=["model"], category="model", version="1.1.2"
)
class LoRACollectionLoader(BaseInvocation):
"""Applies a collection of LoRAs to the provided UNet and CLIP models."""
@@ -322,10 +322,10 @@ class SDXLLoRALoaderOutput(BaseInvocationOutput):
@invocation(
"sdxl_lora_loader",
title="LoRA Model - SDXL",
title="Apply LoRA - SDXL",
tags=["lora", "model"],
category="model",
version="1.0.4",
version="1.0.5",
)
class SDXLLoRALoaderInvocation(BaseInvocation):
"""Apply selected lora to unet and text_encoder."""
@@ -402,10 +402,10 @@ class SDXLLoRALoaderInvocation(BaseInvocation):
@invocation(
"sdxl_lora_collection_loader",
title="LoRA Collection - SDXL",
title="Apply LoRA Collection - SDXL",
tags=["model"],
category="model",
version="1.1.1",
version="1.1.2",
)
class SDXLLoRACollectionLoader(BaseInvocation):
"""Applies a collection of SDXL LoRAs to the provided UNet and CLIP models."""

View File

@@ -563,14 +563,20 @@ class CheckpointProbeBase(ProbeBase):
if base_type == BaseModelType.Flux:
in_channels = state_dict["img_in.weight"].shape[1]
if in_channels == 64:
return ModelVariantType.Normal
elif in_channels == 384:
# FLUX Model variant types are distinguished by input channels:
# - Unquantized Dev and Schnell have in_channels=64
# - BNB-NF4 Dev and Schnell have in_channels=1
# - FLUX Fill has in_channels=384
# - Unsure of quantized FLUX Fill models
# - Unsure of GGUF-quantized models
if in_channels == 384:
# This is a FLUX Fill model. FLUX Fill needs special handling throughout the application. The variant
# type is used to determine whether to use the fill model or the base model.
return ModelVariantType.Inpaint
else:
raise InvalidModelConfigException(
f"Unexpected in_channels (in_channels={in_channels}) for FLUX model at {self.model_path}."
)
# Fall back on "normal" variant type for all other FLUX models.
return ModelVariantType.Normal
in_channels = state_dict["model.diffusion_model.input_blocks.0.0.weight"].shape[1]
if in_channels == 9:

View File

@@ -114,7 +114,9 @@
"layout": "Layout",
"board": "Ordner",
"combinatorial": "Kombinatorisch",
"saveChanges": "Änderungen speichern"
"saveChanges": "Änderungen speichern",
"error_withCount_one": "{{count}} Fehler",
"error_withCount_other": "{{count}} Fehler"
},
"gallery": {
"galleryImageSize": "Bildgröße",
@@ -764,10 +766,10 @@
"layerCopiedToClipboard": "Ebene in die Zwischenablage kopiert",
"sentToCanvas": "An Leinwand gesendet",
"problemDeletingWorkflow": "Problem beim Löschen des Arbeitsablaufs",
"uploadFailedInvalidUploadDesc_withCount_one": "Es darf maximal 1 PNG- oder JPEG-Bild sein.",
"uploadFailedInvalidUploadDesc_withCount_other": "Es dürfen maximal {{count}} PNG- oder JPEG-Bilder sein.",
"uploadFailedInvalidUploadDesc_withCount_one": "Darf maximal 1 PNG-, JPEG- oder WEBP-Bild sein.",
"uploadFailedInvalidUploadDesc_withCount_other": "Dürfen maximal {{count}} PNG-, JPEG- oder WEBP-Bild sein.",
"problemRetrievingWorkflow": "Problem beim Abrufen des Arbeitsablaufs",
"uploadFailedInvalidUploadDesc": "Müssen PNG- oder JPEG-Bilder sein.",
"uploadFailedInvalidUploadDesc": "Müssen PNG-, JPEG- oder WEBP-Bilder sein.",
"pasteSuccess": "Eingefügt in {{destination}}",
"pasteFailed": "Einfügen fehlgeschlagen",
"unableToCopy": "Kopieren nicht möglich",
@@ -1259,7 +1261,6 @@
"nodePack": "Knoten-Pack",
"loadWorkflow": "Lade Workflow",
"snapToGrid": "Am Gitternetz einrasten",
"unknownOutput": "Unbekannte Ausgabe: {{name}}",
"updateNode": "Knoten updaten",
"edge": "Rand / Kante",
"sourceNodeDoesNotExist": "Ungültiger Rand: Quell- / Ausgabe-Knoten {{node}} existiert nicht",
@@ -1325,7 +1326,9 @@
"description": "Beschreibung",
"loadWorkflowDesc": "Arbeitsablauf laden?",
"loadWorkflowDesc2": "Ihr aktueller Arbeitsablauf enthält nicht gespeicherte Änderungen.",
"loadingTemplates": "Lade {{name}}"
"loadingTemplates": "Lade {{name}}",
"missingSourceOrTargetHandle": "Fehlender Quell- oder Zielgriff",
"missingSourceOrTargetNode": "Fehlender Quell- oder Zielknoten"
},
"hrf": {
"enableHrf": "Korrektur für hohe Auflösungen",

View File

@@ -194,7 +194,9 @@
"combinatorial": "Combinatorial",
"layout": "Layout",
"row": "Row",
"column": "Column"
"column": "Column",
"value": "Value",
"label": "Label"
},
"hrf": {
"hrf": "High Resolution Fix",
@@ -1014,7 +1016,10 @@
"unknownNodeType": "Unknown node type",
"unknownTemplate": "Unknown Template",
"unknownInput": "Unknown input: {{name}}",
"unknownOutput": "Unknown output: {{name}}",
"missingField_withName": "Missing field \"{{name}}\"",
"unexpectedField_withName": "Unexpected field \"{{name}}\"",
"unknownField_withName": "Unknown field \"{{name}}\"",
"unknownFieldEditWorkflowToFix_withName": "Workflow contains an unknown field \"{{name}}\".\nEdit the workflow to fix the issue.",
"updateNode": "Update Node",
"updateApp": "Update App",
"loadingTemplates": "Loading {{name}}",
@@ -1299,7 +1304,8 @@
"problemDeletingWorkflow": "Problem Deleting Workflow",
"unableToCopy": "Unable to Copy",
"unableToCopyDesc": "Your browser does not support clipboard access. Firefox users may be able to fix this by following ",
"unableToCopyDesc_theseSteps": "these steps"
"unableToCopyDesc_theseSteps": "these steps",
"fluxFillIncompatibleWithT2IAndI2I": "FLUX Fill is not compatible with Text to Image or Image to Image. Use other FLUX models for these tasks."
},
"popovers": {
"clipSkip": {
@@ -1740,6 +1746,7 @@
"openLibrary": "Open Library",
"workflowThumbnail": "Workflow Thumbnail",
"saveChanges": "Save Changes",
"emptyStringPlaceholder": "<empty string>",
"builder": {
"deleteAllElements": "Delete All Form Elements",
"resetAllNodeFields": "Reset All Node Fields",
@@ -1764,6 +1771,9 @@
"singleLine": "Single Line",
"multiLine": "Multi Line",
"slider": "Slider",
"dropdown": "Dropdown",
"addOption": "Add Option",
"resetOptions": "Reset Options",
"both": "Both",
"emptyRootPlaceholderViewMode": "Click Edit to start building a form for this workflow.",
"emptyRootPlaceholderEditMode": "Drag a form element or node field here to get started.",
@@ -1950,7 +1960,8 @@
"rgNegativePromptNotSupported": "Negative Prompt not supported for selected base model",
"rgReferenceImagesNotSupported": "regional Reference Images not supported for selected base model",
"rgAutoNegativeNotSupported": "Auto-Negative not supported for selected base model",
"rgNoRegion": "no region drawn"
"rgNoRegion": "no region drawn",
"fluxFillIncompatibleWithControlLoRA": "Control LoRA is not compatible with FLUX Fill"
},
"errors": {
"unableToFindImage": "Unable to find image",
@@ -2333,7 +2344,7 @@
"whatsNewInInvoke": "What's New in Invoke",
"items": [
"Workflows: New and improved Workflow Library.",
"FLUX: Support for FLUX Redux in Workflows and Canvas."
"FLUX: Support for FLUX Redux & FLUX Fill in Workflows and Canvas."
],
"readReleaseNotes": "Read Release Notes",
"watchRecentReleaseVideos": "Watch Recent Release Videos",

View File

@@ -1653,7 +1653,6 @@
"collectionFieldType": "{{name}} (Collection)",
"newWorkflow": "Nouveau Workflow",
"reorderLinearView": "Réorganiser la vue linéaire",
"unknownOutput": "Sortie inconnue : {{name}}",
"outputFieldTypeParseError": "Impossible d'analyser le type du champ de sortie {{node}}.{{field}} ({{message}})",
"unsupportedMismatchedUnion": "type CollectionOrScalar non concordant avec les types de base {{firstType}} et {{secondType}}",
"unableToParseFieldType": "impossible d'analyser le type de champ",

View File

@@ -779,7 +779,8 @@
"enableModelDescriptions": "Abilita le descrizioni dei modelli nei menu a discesa",
"modelDescriptionsDisabled": "Descrizioni dei modelli nei menu a discesa disabilitate",
"modelDescriptionsDisabledDesc": "Le descrizioni dei modelli nei menu a discesa sono state disabilitate. Abilitale nelle Impostazioni.",
"showDetailedInvocationProgress": "Mostra dettagli avanzamento"
"showDetailedInvocationProgress": "Mostra dettagli avanzamento",
"enableHighlightFocusedRegions": "Evidenzia le regioni interessate"
},
"toast": {
"uploadFailed": "Caricamento fallito",
@@ -968,7 +969,6 @@
"unableToGetWorkflowVersion": "Impossibile ottenere la versione dello schema del flusso di lavoro",
"nodePack": "Pacchetto di nodi",
"unableToExtractSchemaNameFromRef": "Impossibile estrarre il nome dello schema dal riferimento",
"unknownOutput": "Output sconosciuto: {{name}}",
"unknownNodeType": "Tipo di nodo sconosciuto",
"targetNodeDoesNotExist": "Connessione non valida: il nodo di destinazione/input {{node}} non esiste",
"unknownFieldType": "$t(nodes.unknownField) tipo: {{type}}",
@@ -1776,7 +1776,9 @@
"text": "Testo",
"numberInput": "Ingresso numerico",
"containerRowLayout": "Contenitore (disposizione riga)",
"containerColumnLayout": "Contenitore (disposizione colonna)"
"containerColumnLayout": "Contenitore (disposizione colonna)",
"minimum": "Minimo",
"maximum": "Massimo"
},
"loadMore": "Carica altro",
"searchPlaceholder": "Cerca per nome, descrizione o etichetta",
@@ -1791,7 +1793,8 @@
"private": "Privato",
"deselectAll": "Deseleziona tutto",
"noRecentWorkflows": "Nessun flusso di lavoro recente",
"view": "Visualizza"
"view": "Visualizza",
"recommended": "Consigliato per te"
},
"accordions": {
"compositing": {
@@ -2350,8 +2353,8 @@
"watchRecentReleaseVideos": "Guarda i video su questa versione",
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
"items": [
"Gestione della memoria: nuova impostazione per gli utenti con GPU Nvidia per ridurre l'utilizzo della VRAM.",
"Prestazioni: continui miglioramenti alle prestazioni e alla reattività complessive dell'applicazione."
"Flussi di lavoro: nuova e migliorata libreria dei flussi di lavoro.",
"FLUX: supporto per FLUX Redux in Flussi di lavoro e Tela."
]
},
"system": {

View File

@@ -425,7 +425,6 @@
"newWorkflow": "Nieuwe werkstroom",
"unknownErrorValidatingWorkflow": "Onbekende fout bij valideren werkstroom",
"unsupportedAnyOfLength": "te veel union-leden ({{count}})",
"unknownOutput": "Onbekende uitvoer: {{name}}",
"viewMode": "Gebruik in lineaire weergave",
"unableToExtractSchemaNameFromRef": "fout bij het extraheren van de schemanaam via de ref",
"unsupportedMismatchedUnion": "niet-overeenkomende soort CollectionOrScalar met basissoorten {{firstType}} en {{secondType}}",

View File

@@ -879,7 +879,6 @@
"unableToExtractSchemaNameFromRef": "невозможно извлечь имя схемы из ссылки",
"executionStateError": "Ошибка",
"prototypeDesc": "Этот вызов является прототипом. Он может претерпевать изменения при обновлении приложения и может быть удален в любой момент.",
"unknownOutput": "Неизвестный вывод: {{name}}",
"executionStateCompleted": "Выполнено",
"node": "Узел",
"workflowAuthor": "Автор",

View File

@@ -236,7 +236,8 @@
"layout": "Bố Cục",
"row": "Hàng",
"board": "Bảng",
"saveChanges": "Lưu Thay Đổi"
"saveChanges": "Lưu Thay Đổi",
"error_withCount_other": "{{count}} lỗi"
},
"prompt": {
"addPromptTrigger": "Thêm Prompt Trigger",
@@ -769,7 +770,8 @@
"urlForbiddenErrorMessage": "Bạn có thể cần yêu cầu quyền truy cập từ trang web đang cung cấp model.",
"urlUnauthorizedErrorMessage": "Bạn có thể cần thiếp lập một token API để dùng được model này.",
"fluxRedux": "FLUX Redux",
"sigLip": "SigLIP"
"sigLip": "SigLIP",
"llavaOnevision": "LLaVA OneVision"
},
"metadata": {
"guidance": "Hướng Dẫn",
@@ -892,7 +894,6 @@
"targetNodeFieldDoesNotExist": "Kết nối không phù hợp: đích đến/đầu vào của vùng {{node}}.{{field}} không tồn tại",
"missingTemplate": "Node không hợp lệ: node {{node}} thuộc loại {{type}} bị thiếu mẫu trình bày (chưa tải?)",
"unsupportedMismatchedUnion": "Dạng số lượng dữ liệu không khớp với {{firstType}} và {{secondType}}",
"unknownOutput": "Đầu Ra Không Rõ: {{name}}",
"betaDesc": "Trình kích hoạt này vẫn trong giai đoạn beta. Cho đến khi ổn định, nó có thể phá hỏng thay đổi trong khi cập nhật ứng dụng. Chúng tôi dự định hỗ trợ trình kích hoạt này về lâu dài.",
"cannotConnectInputToInput": "Không thế kết nối đầu vào với đầu vào",
"showEdgeLabelsHelp": "Hiển thị tên trên kết nối, chỉ ra những node được kết nối",
@@ -1618,7 +1619,8 @@
"displayInProgress": "Hiển Thị Hình Ảnh Đang Xử Lý",
"intermediatesClearedFailed": "Có Vấn Đề Khi Dọn Sạch Sản Phẩm Trung Gian",
"enableInvisibleWatermark": "Bật Chế Độ Ẩn Watermark",
"showDetailedInvocationProgress": "Hiện Dữ Liệu Xử Lý"
"showDetailedInvocationProgress": "Hiện Dữ Liệu Xử Lý",
"enableHighlightFocusedRegions": "Nhấn Mạnh Khu Vực Chỉ Định"
},
"sdxl": {
"loading": "Đang Tải...",
@@ -2288,7 +2290,11 @@
"container": "Hộp Chứa",
"heading": "Đầu Dòng",
"text": "Văn Bản",
"divider": "Gạch Chia"
"divider": "Gạch Chia",
"minimum": "Tối Thiểu",
"maximum": "Tối Đa",
"containerRowLayout": "Hộp Chứa (bố cục hàng)",
"containerColumnLayout": "Hộp Chứa (bố cục cột)"
},
"yourWorkflows": "Workflow Của Bạn",
"browseWorkflows": "Khám Phá Workflow",
@@ -2300,7 +2306,11 @@
"filterByTags": "Lọc Theo Nhãn",
"recentlyOpened": "Mở Gần Đây",
"private": "Cá Nhân",
"loadMore": "Tải Thêm"
"loadMore": "Tải Thêm",
"view": "Xem",
"deselectAll": "Huỷ Chọn Tất Cả",
"noRecentWorkflows": "Không Có Workflows Gần Đây",
"recommended": "Có Thể Bạn Sẽ Cần"
},
"upscaling": {
"missingUpscaleInitialImage": "Thiếu ảnh dùng để upscale",
@@ -2327,7 +2337,8 @@
"gettingStartedSeries": "Cần thêm hướng dẫn? Xem thử <LinkComponent>Bắt Đầu Làm Quen</LinkComponent> để biết thêm mẹo khai thác toàn bộ tiềm năng của Invoke Studio.",
"toGetStarted": "Để bắt đầu, hãy nhập lệnh vào hộp và nhấp chuột vào <StrongComponent>Kích Hoạt</StrongComponent> để tạo ra bức ảnh đầu tiên. Chọn một mẫu trình bày cho lệnh để cải thiện kết quả. Bạn có thể chọn để lưu ảnh trực tiếp vào <StrongComponent>Thư Viện Ảnh</StrongComponent> hoặc chỉnh sửa chúng ở <StrongComponent>Canvas</StrongComponent>.",
"noModelsInstalled": "Dường như bạn chưa tải model nào cả! Bạn có thể <DownloadStarterModelsButton>tải xuống các model khởi đầu</DownloadStarterModelsButton> hoặc <ImportModelsButton>nhập vào thêm model</ImportModelsButton>.",
"lowVRAMMode": "Cho hiệu suất tốt nhất, hãy làm theo <LinkComponent>hướng dẫn VRAM Thấp</LinkComponent> của chúng tôi."
"lowVRAMMode": "Cho hiệu suất tốt nhất, hãy làm theo <LinkComponent>hướng dẫn VRAM Thấp</LinkComponent> của chúng tôi.",
"toGetStartedWorkflow": "Để bắt đầu, hãy điền vào khu vực bên trái và bấm <StrongComponent>Kích Hoạt</StrongComponent> nhằm tạo sinh ảnh. Muốn khám phá thêm workflow? Nhấp vào <StrongComponent>icon thư mục</StrongComponent> nằm cạnh tiêu đề workflow để xem một dãy các mẫu trình bày khác."
},
"whatsNew": {
"whatsNewInInvoke": "Có Gì Mới Ở Invoke",
@@ -2335,8 +2346,8 @@
"watchRecentReleaseVideos": "Xem Video Phát Hành Mới Nhất",
"watchUiUpdatesOverview": "Xem Tổng Quan Về Những Cập Nhật Cho Giao Diện Người Dùng",
"items": [
"Trình Quản Lý Bộ Nhớ: Thiết lập mới cho người dùng với GPU Nvidia để giảm lượng VRAM sử dụng.",
"Hiệu suất: Các cải thiện tiếp theo nhằm gói gọn hiệu suất và khả năng phản hồi của ứng dụng."
"Workflow: Thư Viện Workflow mới và đã được cải tiến.",
"FLUX: Hỗ trợ FLUX Redux trong Workflow và Canvas."
]
},
"upsell": {

View File

@@ -908,7 +908,6 @@
"unableToGetWorkflowVersion": "无法获取工作流架构版本",
"nodePack": "节点包",
"unableToExtractSchemaNameFromRef": "无法从参考中提取架构名",
"unknownOutput": "未知输出:{{name}}",
"unknownErrorValidatingWorkflow": "验证工作流时出现未知错误",
"collectionFieldType": "{{name}}(合集)",
"unknownNodeType": "未知节点类型",

View File

@@ -4,7 +4,6 @@ import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
import { selectModel } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import {
@@ -19,11 +18,12 @@ import { upperFirst } from 'lodash-es';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiWarningBold } from 'react-icons/pi';
import { selectMainModelConfig } from 'services/api/endpoints/models';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const buildSelectWarnings = (entityIdentifier: CanvasEntityIdentifier, t: TFunction) => {
return createSelector(selectCanvasSlice, selectModel, (canvas, model) => {
return createSelector(selectCanvasSlice, selectMainModelConfig, (canvas, model) => {
// This component is used within a <CanvasEntityStateGate /> so we can safely assume that the entity exists.
// Should never throw.
const entity = selectEntityOrThrow(canvas, entityIdentifier, 'CanvasEntityHeaderWarnings');

View File

@@ -5,7 +5,7 @@ import type {
CanvasReferenceImageState,
CanvasRegionalGuidanceState,
} from 'features/controlLayers/store/types';
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
import type { MainModelConfig } from 'services/api/types';
const WARNINGS = {
UNSUPPORTED_MODEL: 'controlLayers.warnings.unsupportedModel',
@@ -20,13 +20,14 @@ const WARNINGS = {
CONTROL_ADAPTER_NO_MODEL_SELECTED: 'controlLayers.warnings.controlAdapterNoModelSelected',
CONTROL_ADAPTER_INCOMPATIBLE_BASE_MODEL: 'controlLayers.warnings.controlAdapterIncompatibleBaseModel',
CONTROL_ADAPTER_NO_CONTROL: 'controlLayers.warnings.controlAdapterNoControl',
FLUX_FILL_NO_WORKY_WITH_CONTROL_LORA: 'controlLayers.warnings.fluxFillIncompatibleWithControlLoRA',
} as const;
type WarningTKey = (typeof WARNINGS)[keyof typeof WARNINGS];
export const getRegionalGuidanceWarnings = (
entity: CanvasRegionalGuidanceState,
model: ParameterModel | null
model: MainModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -78,7 +79,7 @@ export const getRegionalGuidanceWarnings = (
export const getGlobalReferenceImageWarnings = (
entity: CanvasReferenceImageState,
model: ParameterModel | null
model: MainModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -110,7 +111,7 @@ export const getGlobalReferenceImageWarnings = (
export const getControlLayerWarnings = (
entity: CanvasControlLayerState,
model: ParameterModel | null
model: MainModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -129,6 +130,13 @@ export const getControlLayerWarnings = (
} else if (entity.controlAdapter.model.base !== model.base) {
// Supported model architecture but doesn't match
warnings.push(WARNINGS.CONTROL_ADAPTER_INCOMPATIBLE_BASE_MODEL);
} else if (
model.base === 'flux' &&
model.variant === 'inpaint' &&
entity.controlAdapter.model.type === 'control_lora'
) {
// FLUX inpaint variants are FLUX Fill models - not compatible w/ Control LoRA
warnings.push(WARNINGS.FLUX_FILL_NO_WORKY_WITH_CONTROL_LORA);
}
}
@@ -137,7 +145,7 @@ export const getControlLayerWarnings = (
export const getRasterLayerWarnings = (
_entity: CanvasRasterLayerState,
_model: ParameterModel | null
_model: MainModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -148,7 +156,7 @@ export const getRasterLayerWarnings = (
export const getInpaintMaskWarnings = (
_entity: CanvasInpaintMaskState,
_model: ParameterModel | null
_model: MainModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];

View File

@@ -42,8 +42,7 @@ export class InvalidModelConfigError extends Error {
export const fetchModelConfig = async (key: string): Promise<AnyModelConfig> => {
const { dispatch } = getStore();
try {
const req = dispatch(modelsApi.endpoints.getModelConfig.initiate(key));
req.unsubscribe();
const req = dispatch(modelsApi.endpoints.getModelConfig.initiate(key, { subscribe: false }));
return await req.unwrap();
} catch {
throw new ModelConfigNotFoundError(`Unable to retrieve model config for key ${key}`);
@@ -62,8 +61,9 @@ export const fetchModelConfig = async (key: string): Promise<AnyModelConfig> =>
const fetchModelConfigByAttrs = async (name: string, base: BaseModelType, type: ModelType): Promise<AnyModelConfig> => {
const { dispatch } = getStore();
try {
const req = dispatch(modelsApi.endpoints.getModelConfigByAttrs.initiate({ name, base, type }));
req.unsubscribe();
const req = dispatch(
modelsApi.endpoints.getModelConfigByAttrs.initiate({ name, base, type }, { subscribe: false })
);
return await req.unwrap();
} catch {
throw new ModelConfigNotFoundError(`Unable to retrieve model config for name/base/type ${name}/${base}/${type}`);

View File

@@ -25,7 +25,7 @@ export const ModelTypeFilter = memo(() => {
clip_vision: 'CLIP Vision',
spandrel_image_to_image: t('modelManager.spandrelImageToImage'),
control_lora: t('modelManager.controlLora'),
siglip: t('modelManager.siglip'),
siglip: t('modelManager.sigLip'),
flux_redux: t('modelManager.fluxRedux'),
llava_onevision: t('modelManager.llavaOnevision'),
}),

View File

@@ -8,7 +8,7 @@ import {
Textarea,
} from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
import { useInputFieldDescriptionSafe } from 'features/nodes/hooks/useInputFieldDescriptionSafe';
import { fieldDescriptionChanged } from 'features/nodes/store/nodesSlice';
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { ChangeEvent } from 'react';
@@ -48,7 +48,7 @@ InputFieldDescriptionPopover.displayName = 'InputFieldDescriptionPopover';
const Content = memo(({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const description = useInputFieldDescription(nodeId, fieldName);
const description = useInputFieldDescriptionSafe(nodeId, fieldName);
const onChange = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(fieldDescriptionChanged({ nodeId, fieldName, val: e.target.value }));

View File

@@ -7,7 +7,7 @@ import { InputFieldResetToDefaultValueIconButton } from 'features/nodes/componen
import { useNodeFieldDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { FieldInputTemplate } from 'features/nodes/types/field';
import { memo, useRef } from 'react';
@@ -22,7 +22,7 @@ interface Props {
}
export const InputFieldEditModeNodes = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
const isInvalid = useInputFieldIsInvalid(nodeId, fieldName);
const isConnected = useInputFieldIsConnected(nodeId, fieldName);

View File

@@ -1,23 +1,83 @@
import { InputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder';
import { Flex, Text } from '@invoke-ai/ui-library';
import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper';
import { useInputFieldInstanceExists } from 'features/nodes/hooks/useInputFieldInstanceExists';
import { useInputFieldNameSafe } from 'features/nodes/hooks/useInputFieldNameSafe';
import { useInputFieldTemplateExists } from 'features/nodes/hooks/useInputFieldTemplateExists';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import type { PropsWithChildren, ReactNode } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
fallback?: ReactNode;
formatLabel?: (name: string) => string;
}>;
export const InputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
export const InputFieldGate = memo(({ nodeId, fieldName, children, fallback, formatLabel }: Props) => {
const hasInstance = useInputFieldInstanceExists(nodeId, fieldName);
const hasTemplate = useInputFieldTemplateExists(nodeId, fieldName);
if (!hasTemplate || !hasInstance) {
return <InputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
// fallback may be null, indicating we should render nothing at all - must check for undefined explicitly
if (fallback !== undefined) {
return fallback;
}
return (
<Fallback
nodeId={nodeId}
fieldName={fieldName}
formatLabel={formatLabel}
hasInstance={hasInstance}
hasTemplate={hasTemplate}
/>
);
}
return children;
});
InputFieldGate.displayName = 'InputFieldGate';
const Fallback = memo(
({
nodeId,
fieldName,
formatLabel,
hasTemplate,
hasInstance,
}: {
nodeId: string;
fieldName: string;
formatLabel?: (name: string) => string;
hasTemplate: boolean;
hasInstance: boolean;
}) => {
const { t } = useTranslation();
const name = useInputFieldNameSafe(nodeId, fieldName);
const label = useMemo(() => {
if (formatLabel) {
return formatLabel(name);
}
if (hasTemplate && !hasInstance) {
return t('nodes.missingField_withName', { name });
}
if (!hasTemplate && hasInstance) {
return t('nodes.unexpectedField_withName', { name });
}
return t('nodes.unknownField_withName', { name });
}, [formatLabel, hasInstance, hasTemplate, name, t]);
return (
<InputFieldWrapper>
<Flex w="full" px={1} py={1} justifyContent="center">
<Text fontWeight="semibold" color="error.300" whiteSpace="pre" textAlign="center">
{label}
</Text>
</Flex>
</InputFieldWrapper>
);
}
);
Fallback.displayName = 'Fallback';

View File

@@ -7,7 +7,7 @@ import {
useIsConnectionInProgress,
useIsConnectionStartField,
} from 'features/nodes/hooks/useFieldConnectionState';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import type { FieldInputTemplate } from 'features/nodes/types/field';
@@ -62,7 +62,7 @@ const handleStyles = {
} satisfies CSSProperties;
export const InputFieldHandle = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]);
const isModelField = useMemo(() => isModelFieldType(fieldTemplate.type), [fieldTemplate.type]);

View File

@@ -13,10 +13,11 @@ import { StringGeneratorFieldInputComponent } from 'features/nodes/components/fl
import { IntegerFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput';
import { IntegerFieldInputAndSlider } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInputAndSlider';
import { IntegerFieldSlider } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider';
import { StringFieldDropdown } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldDropdown';
import { StringFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput';
import { StringFieldTextarea } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import {
isBoardFieldInputInstance,
isBoardFieldInputTemplate,
@@ -135,7 +136,7 @@ type Props = {
export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props) => {
const field = useInputFieldInstance(nodeId, fieldName);
const template = useInputFieldTemplate(nodeId, fieldName);
const template = useInputFieldTemplateOrThrow(nodeId, fieldName);
// When deciding which component to render, first we check the type of the template, which is more efficient than the
// instance type check. The instance type check uses zod and is slower.
@@ -151,7 +152,7 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
if (!isStringFieldInputInstance(field)) {
return null;
}
if (settings?.type !== 'string-field-config') {
if (!settings || settings.type !== 'string-field-config') {
if (template.ui_component === 'textarea') {
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
} else {
@@ -162,8 +163,10 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
return <StringFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
} else if (settings.component === 'textarea') {
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
} else if (settings.component === 'dropdown') {
return <StringFieldDropdown nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
} else {
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
assert<Equals<never, typeof settings>>(false, 'Unexpected settings');
}
}

View File

@@ -9,7 +9,7 @@ import {
useIsConnectionStartField,
} from 'features/nodes/hooks/useFieldConnectionState';
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
import { useInputFieldLabelSafe } from 'features/nodes/hooks/useInputFieldLabelSafe';
import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle';
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
import { HANDLE_TOOLTIP_OPEN_DELAY, NO_FIT_ON_DOUBLE_CLICK_CLASS } from 'features/nodes/types/constants';
@@ -43,7 +43,7 @@ interface Props {
export const InputFieldTitle = memo((props: Props) => {
const { nodeId, fieldName, isInvalid, isDragging } = props;
const inputRef = useRef<HTMLInputElement>(null);
const label = useInputFieldLabel(nodeId, fieldName);
const label = useInputFieldLabelSafe(nodeId, fieldName);
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
const { t } = useTranslation();
const isConnected = useInputFieldIsConnected(nodeId, fieldName);

View File

@@ -1,7 +1,7 @@
import { Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library';
import { useInputFieldErrors } from 'features/nodes/hooks/useInputFieldErrors';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import { startCase } from 'lodash-es';
import { memo, useMemo } from 'react';
@@ -16,7 +16,7 @@ export const InputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const fieldInstance = useInputFieldInstance(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
const fieldErrors = useInputFieldErrors(nodeId, fieldName);

View File

@@ -1,27 +0,0 @@
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper';
import { useInputFieldName } from 'features/nodes/hooks/useInputFieldName';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
nodeId: string;
fieldName: string;
};
export const InputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const name = useInputFieldName(nodeId, fieldName);
return (
<InputFieldWrapper>
<FormControl isInvalid={true} alignItems="stretch" justifyContent="center" gap={2} h="full" w="full">
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
{t('nodes.unknownInput', { name })}
</FormLabel>
</FormControl>
</InputFieldWrapper>
);
});
InputFieldUnknownPlaceholder.displayName = 'InputFieldUnknownPlaceholder';

View File

@@ -1,7 +1,10 @@
import { OutputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder';
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
import { useOutputFieldName } from 'features/nodes/hooks/useOutputFieldName';
import { useOutputFieldTemplateExists } from 'features/nodes/hooks/useOutputFieldTemplateExists';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = PropsWithChildren<{
nodeId: string;
@@ -12,10 +15,27 @@ export const OutputFieldGate = memo(({ nodeId, fieldName, children }: Props) =>
const hasTemplate = useOutputFieldTemplateExists(nodeId, fieldName);
if (!hasTemplate) {
return <OutputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
return <Fallback nodeId={nodeId} fieldName={fieldName} />;
}
return children;
});
OutputFieldGate.displayName = 'OutputFieldGate';
const Fallback = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const name = useOutputFieldName(nodeId, fieldName);
return (
<OutputFieldWrapper>
<FormControl isInvalid={true} alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
{t('nodes.unexpectedField_withName', { name })}
</FormLabel>
</FormControl>
</OutputFieldWrapper>
);
});
Fallback.displayName = 'Fallback';

View File

@@ -1,27 +0,0 @@
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
import { useOutputFieldName } from 'features/nodes/hooks/useOutputFieldName';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
nodeId: string;
fieldName: string;
};
export const OutputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const name = useOutputFieldName(nodeId, fieldName);
return (
<OutputFieldWrapper>
<FormControl isInvalid={true} alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
{t('nodes.unknownOutput', { name })}
</FormLabel>
</FormControl>
</OutputFieldWrapper>
);
});
OutputFieldUnknownPlaceholder.displayName = 'OutputFieldUnknownPlaceholder';

View File

@@ -0,0 +1,36 @@
import { Select } from '@invoke-ai/ui-library';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { useStringField } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/useStringField';
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldStringSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
export const StringFieldDropdown = memo(
(
props: FieldComponentProps<
StringFieldInputInstance,
StringFieldInputTemplate,
{ settings: Extract<NodeFieldStringSettings, { component: 'dropdown' }> }
>
) => {
const { value, onChange } = useStringField(props);
return (
<Select
onChange={onChange}
className={`${NO_DRAG_CLASS} ${NO_PAN_CLASS} ${NO_WHEEL_CLASS}`}
isDisabled={props.settings.options.length === 0}
value={value}
>
{props.settings.options.map((choice, i) => (
<option key={`${i}_${choice.value}`} value={choice.value}>
{choice.label || choice.value || `Option ${i + 1}`}
</option>
))}
</Select>
);
}
);
StringFieldDropdown.displayName = 'StringFieldDropdown';

View File

@@ -4,12 +4,21 @@ import { useStringField } from 'features/nodes/components/flow/nodes/Invocation/
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export const StringFieldInput = memo(
(props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
const { value, onChange } = useStringField(props);
const { t } = useTranslation();
return <Input className={`${NO_DRAG_CLASS} ${NO_PAN_CLASS} ${NO_WHEEL_CLASS}`} value={value} onChange={onChange} />;
return (
<Input
className={`${NO_DRAG_CLASS} ${NO_PAN_CLASS} ${NO_WHEEL_CLASS}`}
placeholder={t('workflows.emptyStringPlaceholder')}
value={value}
onChange={onChange}
/>
);
}
);

View File

@@ -4,14 +4,17 @@ import { useStringField } from 'features/nodes/components/flow/nodes/Invocation/
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export const StringFieldTextarea = memo(
(props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
const { t } = useTranslation();
const { value, onChange } = useStringField(props);
return (
<Textarea
className={`${NO_DRAG_CLASS} ${NO_PAN_CLASS} ${NO_WHEEL_CLASS}`}
placeholder={t('workflows.emptyStringPlaceholder')}
value={value}
onChange={onChange}
h="full"

View File

@@ -10,7 +10,7 @@ export const useStringField = (props: FieldComponentProps<StringFieldInputInstan
const dispatch = useAppDispatch();
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
dispatch(
fieldStringValueChanged({
nodeId,

View File

@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Button, Divider, Flex, FormLabel, Grid, GridItem, IconButton, Input } from '@invoke-ai/ui-library';
import { Button, Divider, Flex, Grid, GridItem, IconButton, Input, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
@@ -107,42 +107,6 @@ export const StringFieldCollectionInputComponent = memo(
StringFieldCollectionInputComponent.displayName = 'StringFieldCollectionInputComponent';
type StringListItemContentProps = {
value: string;
index: number;
onRemoveString: (index: number) => void;
onChangeString: (index: number, value: string) => void;
};
const StringListItemContent = memo(({ value, index, onRemoveString, onChangeString }: StringListItemContentProps) => {
const { t } = useTranslation();
const onClickRemove = useCallback(() => {
onRemoveString(index);
}, [index, onRemoveString]);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChangeString(index, e.target.value);
},
[index, onChangeString]
);
return (
<Flex alignItems="center" gap={1}>
<Input size="xs" resize="none" value={value} onChange={onChange} />
<IconButton
size="sm"
variant="link"
alignSelf="stretch"
onClick={onClickRemove}
icon={<PiXBold />}
aria-label={t('common.remove')}
tooltip={t('common.remove')}
/>
</Flex>
);
});
StringListItemContent.displayName = 'StringListItemContent';
type ListItemContentProps = {
value: string;
index: number;
@@ -166,19 +130,26 @@ const ListItemContent = memo(({ value, index, onRemoveString, onChangeString }:
return (
<>
<GridItem>
<FormLabel ps={1} m={0}>
<Text variant="subtext" textAlign="center" minW={8}>
{index + 1}.
</FormLabel>
</Text>
</GridItem>
<GridItem>
<Input size="sm" resize="none" value={value} onChange={onChange} />
<Input
placeholder={t('workflows.emptyStringPlaceholder')}
size="sm"
resize="none"
value={value}
onChange={onChange}
/>
</GridItem>
<GridItem>
<IconButton
tabIndex={-1}
size="sm"
variant="link"
alignSelf="stretch"
minW={8}
minH={8}
onClick={onClickRemove}
icon={<PiXBold />}
aria-label={t('common.delete')}

View File

@@ -1,6 +1,7 @@
import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { ContainerElementSettings } from 'features/nodes/components/sidePanel/builder/ContainerElementSettings';
import { useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
import { NodeFieldElementSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementSettings';
@@ -47,8 +48,16 @@ export const FormElementEditModeHeader = memo(({ element, dragHandleRef, ...rest
<Label element={element} />
<Spacer />
{isContainerElement(element) && <ContainerElementSettings element={element} />}
{isNodeFieldElement(element) && <ZoomToNodeButton element={element} />}
{isNodeFieldElement(element) && <NodeFieldElementSettings element={element} />}
{isNodeFieldElement(element) && (
<InputFieldGate
nodeId={element.data.fieldIdentifier.nodeId}
fieldName={element.data.fieldIdentifier.fieldName}
fallback={null} // Do not render these buttons if the field is not found
>
<ZoomToNodeButton element={element} />
<NodeFieldElementSettings element={element} />
</InputFieldGate>
)}
<RemoveElementButton element={element} />
</Flex>
);

View File

@@ -1,5 +1,4 @@
import { useAppSelector } from 'app/store/storeHooks';
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { NodeFieldElementEditMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode';
import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode';
import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
@@ -15,19 +14,11 @@ export const NodeFieldElement = memo(({ id }: { id: string }) => {
}
if (mode === 'view') {
return (
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
<NodeFieldElementViewMode el={el} />
</InputFieldGate>
);
return <NodeFieldElementViewMode el={el} />;
}
// mode === 'edit'
return (
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
<NodeFieldElementEditMode el={el} />
</InputFieldGate>
);
return <NodeFieldElementEditMode el={el} />;
});
NodeFieldElement.displayName = 'NodeFieldElement';

View File

@@ -2,8 +2,8 @@ import { FormHelperText, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { linkifyOptions, linkifySx } from 'common/components/linkify';
import { useEditable } from 'common/hooks/useEditable';
import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldDescriptionSafe } from 'features/nodes/hooks/useInputFieldDescriptionSafe';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import { fieldDescriptionChanged } from 'features/nodes/store/nodesSlice';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import Linkify from 'linkify-react';
@@ -13,8 +13,8 @@ export const NodeFieldElementDescriptionEditable = memo(({ el }: { el: NodeField
const { data } = el;
const { fieldIdentifier } = data;
const dispatch = useAppDispatch();
const description = useInputFieldDescription(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const description = useInputFieldDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const inputRef = useRef<HTMLTextAreaElement>(null);
const onChange = useCallback(

View File

@@ -1,5 +1,6 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, FormControl } from '@invoke-ai/ui-library';
import { Box, Divider, Flex, FormControl } from '@invoke-ai/ui-library';
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
import { useFormElementDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
@@ -8,9 +9,11 @@ import { FormElementEditModeContent } from 'features/nodes/components/sidePanel/
import { FormElementEditModeHeader } from 'features/nodes/components/sidePanel/builder/FormElementEditModeHeader';
import { NodeFieldElementDescriptionEditable } from 'features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable';
import { NodeFieldElementLabelEditable } from 'features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable';
import { NodeFieldElementStringDropdownSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementStringDropdownSettings';
import { useMouseOverFormField, useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
import type { RefObject } from 'react';
import { memo, useRef } from 'react';
const sx: SystemStyleObject = {
@@ -31,25 +34,11 @@ export const NodeFieldElementEditMode = memo(({ el }: { el: NodeFieldElement })
const dragHandleRef = useRef<HTMLDivElement>(null);
const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
const containerCtx = useContainerContext();
const { id, data } = el;
const { fieldIdentifier, showDescription } = data;
const { id } = el;
return (
<Flex ref={draggableRef} id={id} className={NODE_FIELD_CLASS_NAME} sx={sx} data-parent-layout={containerCtx.layout}>
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
<FormControl flex="1 1 0" orientation="vertical">
<NodeFieldElementLabelEditable el={el} />
<Flex w="full" gap={4}>
<InputFieldRenderer
nodeId={fieldIdentifier.nodeId}
fieldName={fieldIdentifier.fieldName}
settings={data.settings}
/>
</Flex>
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
</FormControl>
</FormElementEditModeContent>
<NodeFieldElementEditModeContent dragHandleRef={dragHandleRef} el={el} isDragging={isDragging} />
<NodeFieldElementOverlay element={el} />
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
</Flex>
@@ -58,6 +47,48 @@ export const NodeFieldElementEditMode = memo(({ el }: { el: NodeFieldElement })
NodeFieldElementEditMode.displayName = 'NodeFieldElementEditMode';
const NodeFieldElementEditModeContent = memo(
({
el,
dragHandleRef,
isDragging,
}: {
el: NodeFieldElement;
dragHandleRef: RefObject<HTMLDivElement>;
isDragging: boolean;
}) => {
const { id, data } = el;
const { fieldIdentifier, showDescription } = data;
return (
<>
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
<FormControl flex="1 1 0" orientation="vertical">
<NodeFieldElementLabelEditable el={el} />
<Flex w="full" gap={4}>
<InputFieldRenderer
nodeId={fieldIdentifier.nodeId}
fieldName={fieldIdentifier.fieldName}
settings={data.settings}
/>
</Flex>
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
{data.settings?.type === 'string-field-config' && data.settings.component === 'dropdown' && (
<>
<Divider />
<NodeFieldElementStringDropdownSettings id={id} settings={data.settings} />
</>
)}
</FormControl>
</InputFieldGate>
</FormElementEditModeContent>
</>
);
}
);
NodeFieldElementEditModeContent.displayName = 'NodeFieldElementEditModeContent';
const nodeFieldOverlaySx: SystemStyleObject = {
position: 'absolute',
top: 0,

View File

@@ -14,42 +14,48 @@ import { useTranslation } from 'react-i18next';
type Props = {
id: string;
config: NodeFieldFloatSettings;
settings: NodeFieldFloatSettings;
nodeId: string;
fieldName: string;
fieldTemplate: FloatFieldInputTemplate;
};
export const NodeFieldElementFloatSettings = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
export const NodeFieldElementFloatSettings = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
return (
<>
<SettingComponent id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingMin id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingMax id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingComponent
id={id}
settings={settings}
nodeId={nodeId}
fieldName={fieldName}
fieldTemplate={fieldTemplate}
/>
<SettingMin id={id} settings={settings} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingMax id={id} settings={settings} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
</>
);
});
NodeFieldElementFloatSettings.displayName = 'NodeFieldElementFloatSettings';
const SettingComponent = memo(({ id, config }: Props) => {
const SettingComponent = memo(({ id, settings }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onChangeComponent = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const newConfig: NodeFieldFloatSettings = {
...config,
const newSettings: NodeFieldFloatSettings = {
...settings,
component: zNumberComponent.parse(e.target.value),
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
},
[config, dispatch, id]
[settings, dispatch, id]
);
return (
<FormControl orientation="vertical">
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
<Select value={config.component} onChange={onChangeComponent} size="sm">
<Select value={settings.component} onChange={onChangeComponent} size="sm">
<option value="number-input">{t('workflows.builder.numberInput')}</option>
<option value="slider">{t('workflows.builder.slider')}</option>
<option value="number-input-and-slider">{t('workflows.builder.both')}</option>
@@ -59,36 +65,36 @@ const SettingComponent = memo(({ id, config }: Props) => {
});
SettingComponent.displayName = 'SettingComponent';
const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
const SettingMin = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const field = useInputFieldInstance<FloatFieldInputInstance>(nodeId, fieldName);
const floatField = useFloatField(nodeId, fieldName, fieldTemplate);
const onToggleSetting = useCallback(() => {
const newConfig: NodeFieldFloatSettings = {
...config,
min: config.min !== undefined ? undefined : floatField.min,
const onToggleOverride = useCallback(() => {
const newSettings: NodeFieldFloatSettings = {
...settings,
min: settings.min !== undefined ? undefined : floatField.min,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
}, [config, dispatch, floatField, id]);
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
}, [settings, dispatch, floatField, id]);
const onChange = useCallback(
(min: number) => {
const newConfig: NodeFieldFloatSettings = {
...config,
const newSettings: NodeFieldFloatSettings = {
...settings,
min,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
// We may need to update the value if it is outside the new min/max range
const constrained = constrainNumber(field.value, floatField, newConfig);
const constrained = constrainNumber(field.value, floatField, newSettings);
if (field.value !== constrained) {
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: constrained }));
}
},
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
[settings, dispatch, field.value, fieldName, floatField, id, nodeId]
);
const constraintMin = useMemo(
@@ -97,20 +103,20 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
);
const constraintMax = useMemo(
() => (config.max ?? floatField.max) - floatField.step,
[config.max, floatField.max, floatField.step]
() => (settings.max ?? floatField.max) - floatField.step,
[settings.max, floatField.max, floatField.step]
);
return (
<FormControl orientation="vertical">
<Flex justifyContent="space-between" w="full" alignItems="center">
<FormLabel m={0}>{t('workflows.builder.minimum')}</FormLabel>
<Switch isChecked={config.min !== undefined} onChange={onToggleSetting} size="sm" />
<Switch isChecked={settings.min !== undefined} onChange={onToggleOverride} size="sm" />
</Flex>
<CompositeNumberInput
w="full"
isDisabled={config.min === undefined}
value={config.min === undefined ? (`${floatField.min} (inherited)` as unknown as number) : config.min}
isDisabled={settings.min === undefined}
value={settings.min === undefined ? (`${floatField.min} (inherited)` as unknown as number) : settings.min}
onChange={onChange}
min={constraintMin}
max={constraintMax}
@@ -121,41 +127,41 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
});
SettingMin.displayName = 'SettingMin';
const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
const SettingMax = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const field = useInputFieldInstance<FloatFieldInputInstance>(nodeId, fieldName);
const floatField = useFloatField(nodeId, fieldName, fieldTemplate);
const onToggleSetting = useCallback(() => {
const newConfig: NodeFieldFloatSettings = {
...config,
max: config.max !== undefined ? undefined : floatField.max,
const onToggleOverride = useCallback(() => {
const newSettings: NodeFieldFloatSettings = {
...settings,
max: settings.max !== undefined ? undefined : floatField.max,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
}, [config, dispatch, floatField, id]);
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
}, [settings, dispatch, floatField, id]);
const onChange = useCallback(
(max: number) => {
const newConfig: NodeFieldFloatSettings = {
...config,
const newSettings: NodeFieldFloatSettings = {
...settings,
max,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
// We may need to update the value if it is outside the new min/max range
const constrained = constrainNumber(field.value, floatField, newConfig);
const constrained = constrainNumber(field.value, floatField, newSettings);
if (field.value !== constrained) {
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: constrained }));
}
},
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
[settings, dispatch, field.value, fieldName, floatField, id, nodeId]
);
const constraintMin = useMemo(
() => (config.min ?? floatField.min) + floatField.step,
[config.min, floatField.min, floatField.step]
() => (settings.min ?? floatField.min) + floatField.step,
[settings.min, floatField.min, floatField.step]
);
const constraintMax = useMemo(
@@ -167,12 +173,12 @@ const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
<FormControl orientation="vertical">
<Flex justifyContent="space-between" w="full" alignItems="center">
<FormLabel m={0}>{t('workflows.builder.maximum')}</FormLabel>
<Switch isChecked={config.max !== undefined} onChange={onToggleSetting} size="sm" />
<Switch isChecked={settings.max !== undefined} onChange={onToggleOverride} size="sm" />
</Flex>
<CompositeNumberInput
w="full"
isDisabled={config.max === undefined}
value={config.max === undefined ? (`${floatField.max} (inherited)` as unknown as number) : config.max}
isDisabled={settings.max === undefined}
value={settings.max === undefined ? (`${floatField.max} (inherited)` as unknown as number) : settings.max}
onChange={onChange}
min={constraintMin}
max={constraintMax}

View File

@@ -15,42 +15,48 @@ import { useTranslation } from 'react-i18next';
type Props = {
id: string;
config: NodeFieldIntegerSettings;
settings: NodeFieldIntegerSettings;
nodeId: string;
fieldName: string;
fieldTemplate: IntegerFieldInputTemplate;
};
export const NodeFieldElementIntegerSettings = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
export const NodeFieldElementIntegerSettings = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
return (
<>
<SettingComponent id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingMin id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingMax id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingComponent
id={id}
settings={settings}
nodeId={nodeId}
fieldName={fieldName}
fieldTemplate={fieldTemplate}
/>
<SettingMin id={id} settings={settings} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
<SettingMax id={id} settings={settings} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
</>
);
});
NodeFieldElementIntegerSettings.displayName = 'NodeFieldElementIntegerSettings';
const SettingComponent = memo(({ id, config }: Props) => {
const SettingComponent = memo(({ id, settings }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onChangeComponent = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const newConfig: NodeFieldIntegerSettings = {
...config,
const newSettings: NodeFieldIntegerSettings = {
...settings,
component: zNumberComponent.parse(e.target.value),
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
},
[config, dispatch, id]
[settings, dispatch, id]
);
return (
<FormControl orientation="vertical">
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
<Select value={config.component} onChange={onChangeComponent} size="sm">
<Select value={settings.component} onChange={onChangeComponent} size="sm">
<option value="number-input">{t('workflows.builder.numberInput')}</option>
<option value="slider">{t('workflows.builder.slider')}</option>
<option value="number-input-and-slider">{t('workflows.builder.both')}</option>
@@ -60,37 +66,37 @@ const SettingComponent = memo(({ id, config }: Props) => {
});
SettingComponent.displayName = 'SettingComponent';
const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
const SettingMin = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const field = useInputFieldInstance<IntegerFieldInputInstance>(nodeId, fieldName);
const integerField = useIntegerField(nodeId, fieldName, fieldTemplate);
const onToggleSetting = useCallback(() => {
const newConfig: NodeFieldIntegerSettings = {
...config,
min: config.min !== undefined ? undefined : integerField.min,
const onToggleOverride = useCallback(() => {
const newSettings: NodeFieldIntegerSettings = {
...settings,
min: settings.min !== undefined ? undefined : integerField.min,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
}, [config, dispatch, integerField.min, id]);
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
}, [settings, dispatch, integerField.min, id]);
const onChange = useCallback(
(min: number) => {
const newConfig: NodeFieldIntegerSettings = {
...config,
const newSettings: NodeFieldIntegerSettings = {
...settings,
min,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
// We may need to update the value if it is outside the new min/max range
const constrained = constrainNumber(field.value, integerField, newConfig);
const constrained = constrainNumber(field.value, integerField, newSettings);
if (field.value !== constrained) {
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: constrained }));
}
},
[config, dispatch, id, field, integerField, nodeId, fieldName]
[settings, dispatch, id, field, integerField, nodeId, fieldName]
);
const constraintMin = useMemo(
@@ -99,20 +105,20 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
);
const constraintMax = useMemo(
() => (config.max ?? integerField.max) - integerField.step,
[config.max, integerField.max, integerField.step]
() => (settings.max ?? integerField.max) - integerField.step,
[settings.max, integerField.max, integerField.step]
);
return (
<FormControl orientation="vertical">
<Flex justifyContent="space-between" w="full" alignItems="center">
<FormLabel m={0}>{t('workflows.builder.minimum')}</FormLabel>
<Switch isChecked={config.min !== undefined} onChange={onToggleSetting} size="sm" />
<Switch isChecked={settings.min !== undefined} onChange={onToggleOverride} size="sm" />
</Flex>
<CompositeNumberInput
w="full"
isDisabled={config.min === undefined}
value={config.min ?? (`${integerField.min} (inherited)` as unknown as number)}
isDisabled={settings.min === undefined}
value={settings.min ?? (`${integerField.min} (inherited)` as unknown as number)}
onChange={onChange}
min={constraintMin}
max={constraintMax}
@@ -123,42 +129,42 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
});
SettingMin.displayName = 'SettingMin';
const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
const SettingMax = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const field = useInputFieldInstance<IntegerFieldInputInstance>(nodeId, fieldName);
const integerField = useIntegerField(nodeId, fieldName, fieldTemplate);
const onToggleSetting = useCallback(() => {
const newConfig: NodeFieldIntegerSettings = {
...config,
max: config.max !== undefined ? undefined : integerField.max,
const onToggleOverride = useCallback(() => {
const newSettings: NodeFieldIntegerSettings = {
...settings,
max: settings.max !== undefined ? undefined : integerField.max,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
}, [config, dispatch, integerField.max, id]);
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
}, [settings, dispatch, integerField.max, id]);
const onChange = useCallback(
(max: number) => {
const newConfig: NodeFieldIntegerSettings = {
...config,
const newSettings: NodeFieldIntegerSettings = {
...settings,
max,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
// We may need to update the value if it is outside the new min/max range
const constrained = constrainNumber(field.value, integerField, newConfig);
const constrained = constrainNumber(field.value, integerField, newSettings);
if (field.value !== constrained) {
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: constrained }));
}
},
[config, dispatch, field.value, fieldName, integerField, id, nodeId]
[settings, dispatch, field.value, fieldName, integerField, id, nodeId]
);
const constraintMin = useMemo(
() => (config.min ?? integerField.min) + integerField.step,
[config.min, integerField.min, integerField.step]
() => (settings.min ?? integerField.min) + integerField.step,
[settings.min, integerField.min, integerField.step]
);
const constraintMax = useMemo(
@@ -170,12 +176,12 @@ const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
<FormControl orientation="vertical">
<Flex justifyContent="space-between" w="full" alignItems="center">
<FormLabel m={0}>{t('workflows.builder.maximum')}</FormLabel>
<Switch isChecked={config.max !== undefined} onChange={onToggleSetting} size="sm" />
<Switch isChecked={settings.max !== undefined} onChange={onToggleOverride} size="sm" />
</Flex>
<CompositeNumberInput
w="full"
isDisabled={config.max === undefined}
value={config.max ?? (`${integerField.max} (inherited)` as unknown as number)}
isDisabled={settings.max === undefined}
value={settings.max ?? (`${integerField.max} (inherited)` as unknown as number)}
onChange={onChange}
min={constraintMin}
max={constraintMax}

View File

@@ -1,15 +1,15 @@
import { Flex, FormLabel, Spacer } from '@invoke-ai/ui-library';
import { NodeFieldElementResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/NodeFieldElementResetToInitialValueIconButton';
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldLabelSafe } from 'features/nodes/hooks/useInputFieldLabelSafe';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { memo, useMemo } from 'react';
export const NodeFieldElementLabel = memo(({ el }: { el: NodeFieldElement }) => {
const { data } = el;
const { fieldIdentifier } = data;
const label = useInputFieldLabel(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const label = useInputFieldLabelSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const _label = useMemo(() => label || fieldTemplate.title, [label, fieldTemplate.title]);

View File

@@ -2,8 +2,8 @@ import { Flex, FormLabel, Input, Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEditable } from 'common/hooks/useEditable';
import { NodeFieldElementResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/NodeFieldElementResetToInitialValueIconButton';
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldLabelSafe } from 'features/nodes/hooks/useInputFieldLabelSafe';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { memo, useCallback, useRef } from 'react';
@@ -12,8 +12,8 @@ export const NodeFieldElementLabelEditable = memo(({ el }: { el: NodeFieldElemen
const { data } = el;
const { fieldIdentifier } = data;
const dispatch = useAppDispatch();
const label = useInputFieldLabel(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const label = useInputFieldLabelSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const inputRef = useRef<HTMLInputElement>(null);
const onChange = useCallback(

View File

@@ -15,7 +15,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { NodeFieldElementFloatSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings';
import { NodeFieldElementIntegerSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings';
import { NodeFieldElementStringSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementStringSettings';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
import {
isFloatFieldInputTemplate,
@@ -36,7 +36,7 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE
const { id, data } = element;
const { showDescription, fieldIdentifier } = data;
const { nodeId, fieldName } = fieldIdentifier;
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
const { t } = useTranslation();
const dispatch = useAppDispatch();
@@ -79,7 +79,7 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE
{settings?.type === 'integer-field-config' && isIntegerFieldInputTemplate(fieldTemplate) && (
<NodeFieldElementIntegerSettings
id={id}
config={settings}
settings={settings}
nodeId={nodeId}
fieldName={fieldName}
fieldTemplate={fieldTemplate}
@@ -88,13 +88,15 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE
{settings?.type === 'float-field-config' && isFloatFieldInputTemplate(fieldTemplate) && (
<NodeFieldElementFloatSettings
id={id}
config={settings}
settings={settings}
nodeId={nodeId}
fieldName={fieldName}
fieldTemplate={fieldTemplate}
/>
)}
{settings?.type === 'string-field-config' && <NodeFieldElementStringSettings id={id} config={settings} />}
{settings?.type === 'string-field-config' && (
<NodeFieldElementStringSettings id={id} settings={settings} />
)}
</Flex>
</PopoverBody>
</PopoverContent>

View File

@@ -0,0 +1,205 @@
import { Button, ButtonGroup, Divider, Flex, Grid, GridItem, IconButton, Input, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import { getDefaultStringOption, type NodeFieldStringDropdownSettings } from 'features/nodes/types/workflow';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiPlusBold, PiXBold } from 'react-icons/pi';
const overlayscrollbarsOptions = getOverlayScrollbarsParams({}).options;
export const NodeFieldElementStringDropdownSettings = memo(
({ id, settings }: { id: string; settings: NodeFieldStringDropdownSettings }) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onRemoveOption = useCallback(
(index: number) => {
const options = [...settings.options];
options.splice(index, 1);
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: { ...settings, options } } }));
},
[settings, dispatch, id]
);
const onChangeOptionValue = useCallback(
(index: number, value: string) => {
if (!settings.options[index]) {
return;
}
const option = { ...settings.options[index], value };
const options = [...settings.options];
options[index] = option;
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: { ...settings, options } } }));
},
[dispatch, id, settings]
);
const onChangeOptionLabel = useCallback(
(index: number, label: string) => {
if (!settings.options[index]) {
return;
}
const option = { ...settings.options[index], label };
const options = [...settings.options];
options[index] = option;
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: { ...settings, options } } }));
},
[dispatch, id, settings]
);
const onAddOption = useCallback(() => {
const options = [...settings.options, getDefaultStringOption()];
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: { ...settings, options } } }));
}, [dispatch, id, settings]);
const onResetOptions = useCallback(() => {
const options = [getDefaultStringOption()];
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: { ...settings, options } } }));
}, [dispatch, id, settings]);
return (
<Flex
className={NO_DRAG_CLASS}
position="relative"
w="full"
h="auto"
maxH={64}
alignItems="stretch"
justifyContent="center"
borderRadius="base"
flexDir="column"
gap={2}
>
<ButtonGroup isAttached={false} w="full">
<Button onClick={onAddOption} variant="ghost" flex={1} leftIcon={<PiPlusBold />}>
{t('workflows.builder.addOption')}
</Button>
<Button onClick={onResetOptions} variant="ghost" flex={1} leftIcon={<PiArrowCounterClockwiseBold />}>
{t('workflows.builder.resetOptions')}
</Button>
</ButtonGroup>
{settings.options.length > 0 && (
<>
<Divider />
<OverlayScrollbarsComponent
className={NO_WHEEL_CLASS}
defer
style={overlayScrollbarsStyles}
options={overlayscrollbarsOptions}
>
<Grid gap={1} gridTemplateColumns="auto 1fr 1fr auto" gridTemplateRows="auto 1fr" alignItems="center">
<GridItem minW={8}>
<Text textAlign="center" variant="subtext">
#
</Text>
</GridItem>
<GridItem>
<Text textAlign="center" variant="subtext">
{t('common.label')}
</Text>
</GridItem>
<GridItem>
<Text textAlign="center" variant="subtext">
{t('common.value')}
</Text>
</GridItem>
<GridItem />
{settings.options.map(({ value, label }, index) => (
<ListItemContent
key={index}
value={value}
label={label}
index={index}
onRemoveOption={onRemoveOption}
onChangeOptionValue={onChangeOptionValue}
onChangeOptionLabel={onChangeOptionLabel}
isRemoveDisabled={settings.options.length <= 1}
/>
))}
</Grid>
</OverlayScrollbarsComponent>
</>
)}
</Flex>
);
}
);
NodeFieldElementStringDropdownSettings.displayName = 'NodeFieldElementStringDropdownSettings';
type ListItemContentProps = {
value: string;
label: string;
index: number;
onRemoveOption: (index: number) => void;
onChangeOptionValue: (index: number, value: string) => void;
onChangeOptionLabel: (index: number, label: string) => void;
isRemoveDisabled: boolean;
};
const ListItemContent = memo(
({
value,
label,
index,
onRemoveOption,
onChangeOptionValue,
onChangeOptionLabel,
isRemoveDisabled,
}: ListItemContentProps) => {
const { t } = useTranslation();
const onClickRemove = useCallback(() => {
onRemoveOption(index);
}, [index, onRemoveOption]);
const onChangeValue = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChangeOptionValue(index, e.target.value);
},
[index, onChangeOptionValue]
);
const onChangeLabel = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChangeOptionLabel(index, e.target.value);
},
[index, onChangeOptionLabel]
);
return (
<>
<GridItem>
<Text variant="subtext" textAlign="center">
{index + 1}.
</Text>
</GridItem>
<GridItem>
<Input size="sm" resize="none" placeholder="label" value={label} onChange={onChangeLabel} />
</GridItem>
<GridItem>
<Input size="sm" resize="none" placeholder="value" value={value} onChange={onChangeValue} />
</GridItem>
<GridItem>
<IconButton
tabIndex={-1}
size="sm"
variant="link"
minW={8}
minH={8}
onClick={onClickRemove}
isDisabled={isRemoveDisabled}
icon={<PiXBold />}
aria-label={t('common.delete')}
/>
</GridItem>
</>
);
}
);
ListItemContent.displayName = 'ListItemContent';

View File

@@ -1,36 +1,55 @@
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
import { type NodeFieldStringSettings, zStringComponent } from 'features/nodes/types/workflow';
import { getDefaultStringOption, type NodeFieldStringSettings, zStringComponent } from 'features/nodes/types/workflow';
import { omit } from 'lodash-es';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const NodeFieldElementStringSettings = memo(
({ id, config }: { id: string; config: NodeFieldStringSettings }) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
type Props = {
id: string;
settings: NodeFieldStringSettings;
};
const onChangeComponent = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const newConfig: NodeFieldStringSettings = {
...config,
component: zStringComponent.parse(e.target.value),
export const NodeFieldElementStringSettings = memo(({ id, settings }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onChangeComponent = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const component = zStringComponent.parse(e.target.value);
if (component === settings.component) {
// no change
return;
}
if (component === 'dropdown') {
// if the component is changing to dropdown, we need to set the options
const newSettings: NodeFieldStringSettings = {
...settings,
component,
options: [getDefaultStringOption()],
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
},
[config, dispatch, id]
);
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
return;
} else {
// if the component is changing from dropdown, we need to remove the options
const newSettings: NodeFieldStringSettings = omit({ ...settings, component }, 'options');
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
}
},
[settings, dispatch, id]
);
return (
<FormControl>
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
<Select value={config.component} onChange={onChangeComponent} size="sm">
<option value="input">{t('workflows.builder.singleLine')}</option>
<option value="textarea">{t('workflows.builder.multiLine')}</option>
</Select>
</FormControl>
);
}
);
return (
<FormControl>
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
<Select value={settings.component} onChange={onChangeComponent} size="sm">
<option value="input">{t('workflows.builder.singleLine')}</option>
<option value="textarea">{t('workflows.builder.multiLine')}</option>
<option value="dropdown">{t('workflows.builder.dropdown')}</option>
</Select>
</FormControl>
);
});
NodeFieldElementStringSettings.displayName = 'NodeFieldElementStringSettings';

View File

@@ -1,15 +1,17 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, FormControl, FormHelperText } from '@invoke-ai/ui-library';
import { linkifyOptions, linkifySx } from 'common/components/linkify';
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
import { NodeFieldElementLabel } from 'features/nodes/components/sidePanel/builder/NodeFieldElementLabel';
import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldDescriptionSafe } from 'features/nodes/hooks/useInputFieldDescriptionSafe';
import { useInputFieldTemplateOrThrow, useInputFieldTemplateSafe } from 'features/nodes/hooks/useInputFieldTemplate';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
import Linkify from 'linkify-react';
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const sx: SystemStyleObject = {
'&[data-parent-layout="column"]': {
@@ -25,16 +27,23 @@ const sx: SystemStyleObject = {
},
};
const useFormatFallbackLabel = () => {
const { t } = useTranslation();
const formatLabel = useCallback((name: string) => t('nodes.unknownFieldEditWorkflowToFix_withName', { name }), [t]);
return formatLabel;
};
export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement }) => {
const { id, data } = el;
const { fieldIdentifier, showDescription } = data;
const description = useInputFieldDescription(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const description = useInputFieldDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplateSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const containerCtx = useContainerContext();
const formatFallbackLabel = useFormatFallbackLabel();
const _description = useMemo(
() => description || fieldTemplate.description,
[description, fieldTemplate.description]
() => description || fieldTemplate?.description || '',
[description, fieldTemplate?.description]
);
return (
@@ -45,22 +54,45 @@ export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement })
data-parent-layout={containerCtx.layout}
data-with-description={showDescription && !!_description}
>
<FormControl flex="1 1 0" orientation="vertical">
<NodeFieldElementLabel el={el} />
<Flex w="full" gap={4}>
<InputFieldRenderer
nodeId={fieldIdentifier.nodeId}
fieldName={fieldIdentifier.fieldName}
settings={data.settings}
/>
</Flex>
{showDescription && _description && (
<FormHelperText sx={linkifySx}>
<Linkify options={linkifyOptions}>{_description}</Linkify>
</FormHelperText>
)}
</FormControl>
<InputFieldGate
nodeId={el.data.fieldIdentifier.nodeId}
fieldName={el.data.fieldIdentifier.fieldName}
formatLabel={formatFallbackLabel}
>
<NodeFieldElementViewModeContent el={el} />
</InputFieldGate>
</Flex>
);
});
NodeFieldElementViewMode.displayName = 'NodeFieldElementViewMode';
const NodeFieldElementViewModeContent = memo(({ el }: { el: NodeFieldElement }) => {
const { data } = el;
const { fieldIdentifier, showDescription } = data;
const description = useInputFieldDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const _description = useMemo(
() => description || fieldTemplate.description,
[description, fieldTemplate.description]
);
return (
<FormControl flex="1 1 0" orientation="vertical">
<NodeFieldElementLabel el={el} />
<Flex w="full" gap={4}>
<InputFieldRenderer
nodeId={fieldIdentifier.nodeId}
fieldName={fieldIdentifier.fieldName}
settings={data.settings}
/>
</Flex>
{showDescription && _description && (
<FormHelperText sx={linkifySx}>
<Linkify options={linkifyOptions}>{_description}</Linkify>
</FormHelperText>
)}
</FormControl>
);
});
NodeFieldElementViewModeContent.displayName = 'NodeFieldElementViewModeContent';

View File

@@ -1,6 +1,6 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import { formElementAdded, selectFormRootElementId } from 'features/nodes/store/workflowSlice';
import { buildNodeFieldElement } from 'features/nodes/types/workflow';
import { useCallback } from 'react';
@@ -8,7 +8,7 @@ import { useCallback } from 'react';
export const useAddNodeFieldToRoot = (nodeId: string, fieldName: string) => {
const dispatch = useAppDispatch();
const rootElementId = useAppSelector(selectFormRootElementId);
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
const field = useInputFieldInstance(nodeId, fieldName);
const addNodeFieldToRoot = useCallback(() => {

View File

@@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
@@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
export const useInputFieldDefaultValue = (nodeId: string, fieldName: string) => {
const dispatch = useAppDispatch();
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
const selectIsChanged = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {

View File

@@ -1,22 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useInputFieldDescription = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return '';
}
return node?.data.inputs[fieldName]?.description ?? '';
}),
[fieldName, nodeId]
);
const notes = useAppSelector(selector);
return notes;
};

View File

@@ -0,0 +1,26 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
/**
* Gets the user-defined description of an input field for a given node.
*
* If the node doesn't exist or is not an invocation node, an empty string is returned.
*
* @param nodeId The ID of the node
* @param fieldName The name of the field
*/
export const useInputFieldDescriptionSafe = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createSelector(
selectNodesSlice,
(nodes) => selectFieldInputInstanceSafe(nodes, nodeId, fieldName)?.description ?? ''
),
[fieldName, nodeId]
);
const description = useAppSelector(selector);
return description;
};

View File

@@ -1,18 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useInputFieldLabel = (nodeId: string, fieldName: string): string => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
return selectFieldInputInstanceSafe(nodes, nodeId, fieldName)?.label ?? '';
}),
[fieldName, nodeId]
);
const label = useAppSelector(selector);
return label;
};

View File

@@ -0,0 +1,24 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
/**
* Gets the user-defined label of an input field for a given node.
*
* If the node doesn't exist or is not an invocation node, an empty string is returned.
*
* @param nodeId The ID of the node
* @param fieldName The name of the field
*/
export const useInputFieldLabelSafe = (nodeId: string, fieldName: string): string => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => selectFieldInputInstanceSafe(nodes, nodeId, fieldName)?.label ?? ''),
[fieldName, nodeId]
);
const label = useAppSelector(selector);
return label;
};

View File

@@ -5,7 +5,7 @@ import { $templates } from 'features/nodes/store/nodesSlice';
import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useInputFieldName = (nodeId: string, fieldName: string) => {
export const useInputFieldNameSafe = (nodeId: string, fieldName: string) => {
const templates = useStore($templates);
const selector = useMemo(

View File

@@ -3,7 +3,16 @@ import type { FieldInputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useInputFieldTemplate = (nodeId: string, fieldName: string): FieldInputTemplate => {
/**
* Returns the template for a specific input field of a node.
*
* **Note:** This hook will throw an error if the template for the input field is not found.
*
* @param nodeId - The ID of the node.
* @param fieldName - The name of the input field.
* @throws Will throw an error if the template for the input field is not found.
*/
export const useInputFieldTemplateOrThrow = (nodeId: string, fieldName: string): FieldInputTemplate => {
const template = useNodeTemplate(nodeId);
const fieldTemplate = useMemo(() => {
const _fieldTemplate = template.inputs[fieldName];
@@ -12,3 +21,17 @@ export const useInputFieldTemplate = (nodeId: string, fieldName: string): FieldI
}, [fieldName, template.inputs]);
return fieldTemplate;
};
/**
* Returns the template for a specific input field of a node.
*
* **Note:** This function is a safe version of `useInputFieldTemplate` and will not throw an error if the template is not found.
*
* @param nodeId - The ID of the node.
* @param fieldName - The name of the input field.
*/
export const useInputFieldTemplateSafe = (nodeId: string, fieldName: string): FieldInputTemplate | null => {
const template = useNodeTemplate(nodeId);
const fieldTemplate = useMemo(() => template.inputs[fieldName] ?? null, [fieldName, template.inputs]);
return fieldTemplate;
};

View File

@@ -91,14 +91,37 @@ const zNodeFieldIntegerSettings = z.object({
export type NodeFieldIntegerSettings = z.infer<typeof zNodeFieldIntegerSettings>;
export const getIntegerFieldSettingsDefaults = (): NodeFieldIntegerSettings => zNodeFieldIntegerSettings.parse({});
export const zStringComponent = z.enum(['input', 'textarea']);
const zStringOption = z
.object({
label: z.string(),
value: z.string(),
})
.default({ label: '', value: '' });
type StringOption = z.infer<typeof zStringOption>;
export const getDefaultStringOption = (): StringOption => ({ label: '', value: '' });
export const zStringComponent = z.enum(['input', 'textarea', 'dropdown']);
const STRING_FIELD_CONFIG_TYPE = 'string-field-config';
const zNodeFieldStringSettings = z.object({
const zNodeFieldStringInputSettings = z.object({
type: z.literal(STRING_FIELD_CONFIG_TYPE).default(STRING_FIELD_CONFIG_TYPE),
component: zStringComponent.default('input'),
component: z.literal('input').default('input'),
});
const zNodeFieldStringTextareaSettings = z.object({
type: z.literal(STRING_FIELD_CONFIG_TYPE).default(STRING_FIELD_CONFIG_TYPE),
component: z.literal('textarea').default('textarea'),
});
const zNodeFieldStringDropdownSettings = z.object({
type: z.literal(STRING_FIELD_CONFIG_TYPE).default(STRING_FIELD_CONFIG_TYPE),
component: z.literal('dropdown').default('dropdown'),
options: z.array(zStringOption),
});
export type NodeFieldStringDropdownSettings = z.infer<typeof zNodeFieldStringDropdownSettings>;
const zNodeFieldStringSettings = z.union([
zNodeFieldStringInputSettings,
zNodeFieldStringTextareaSettings,
zNodeFieldStringDropdownSettings,
]);
export type NodeFieldStringSettings = z.infer<typeof zNodeFieldStringSettings>;
export const getStringFieldSettingsDefaults = (): NodeFieldStringSettings => zNodeFieldStringSettings.parse({});
export const getStringFieldSettingsDefaults = (): NodeFieldStringSettings => zNodeFieldStringInputSettings.parse({});
const zNodeFieldData = z.object({
fieldIdentifier: zFieldIdentifier,

View File

@@ -4,9 +4,8 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasControlLayerState, Rect } from 'features/controlLayers/store/types';
import { getControlLayerWarnings } from 'features/controlLayers/store/validators';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
import { serializeError } from 'serialize-error';
import type { ImageDTO, Invocation } from 'services/api/types';
import type { ImageDTO, Invocation, MainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
const log = logger('system');
@@ -17,7 +16,7 @@ type AddControlNetsArg = {
g: Graph;
rect: Rect;
collector: Invocation<'collect'>;
model: ParameterModel;
model: MainModelConfig;
};
type AddControlNetsResult = {
@@ -66,7 +65,7 @@ type AddT2IAdaptersArg = {
g: Graph;
rect: Rect;
collector: Invocation<'collect'>;
model: ParameterModel;
model: MainModelConfig;
};
type AddT2IAdaptersResult = {
@@ -114,7 +113,7 @@ type AddControlLoRAArg = {
entities: CanvasControlLayerState[];
g: Graph;
rect: Rect;
model: ParameterModel;
model: MainModelConfig;
denoise: Invocation<'flux_denoise'>;
};
@@ -129,9 +128,9 @@ export const addControlLoRA = async ({ manager, entities, g, rect, model, denois
// No valid control LoRA found
return;
}
if (validControlLayers.length > 1) {
throw new Error('Cannot add more than one FLUX control LoRA.');
}
assert(model.variant !== 'inpaint', 'FLUX Control LoRA is not compatible with FLUX Fill.');
assert(validControlLayers.length <= 1, 'Cannot add more than one FLUX control LoRA.');
const getImageDTOResult = await withResultAsync(() => {
const adapter = manager.adapters.controlLayers.get(validControlLayer.id);

View File

@@ -0,0 +1,202 @@
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { Dimensions } from 'features/controlLayers/store/types';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { isEqual } from 'lodash-es';
import type { Invocation } from 'services/api/types';
type AddFLUXFillArg = {
state: RootState;
g: Graph;
manager: CanvasManager;
l2i: Invocation<'flux_vae_decode'>;
denoise: Invocation<'flux_denoise'>;
originalSize: Dimensions;
scaledSize: Dimensions;
};
export const addFLUXFill = async ({
state,
g,
manager,
l2i,
denoise,
originalSize,
scaledSize,
}: AddFLUXFillArg): Promise<Invocation<'invokeai_img_blend' | 'apply_mask_to_image'>> => {
// FLUX Fill always fully denoises
denoise.denoising_start = 0;
denoise.denoising_end = 1;
const params = selectParamsSlice(state);
const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, bbox.rect, {
is_intermediate: true,
silent: true,
});
const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
const maskImage = await manager.compositor.getCompositeImageDTO(inpaintMaskAdapters, bbox.rect, {
is_intermediate: true,
silent: true,
});
const fluxFill = g.addNode({ type: 'flux_fill', id: getPrefixedId('flux_fill') });
if (!isEqual(scaledSize, originalSize)) {
// Scale before processing requires some resizing
// Combine the inpaint mask and the initial image's alpha channel into a single mask
const maskAlphaToMask = g.addNode({
id: getPrefixedId('alpha_to_mask'),
type: 'tomask',
image: { image_name: maskImage.image_name },
invert: !canvasSettings.preserveMask,
});
const initialImageAlphaToMask = g.addNode({
id: getPrefixedId('image_alpha_to_mask'),
type: 'tomask',
image: { image_name: initialImage.image_name },
});
const maskCombine = g.addNode({
id: getPrefixedId('mask_combine'),
type: 'mask_combine',
});
g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1');
g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2');
// Resize the combined and initial image to the scaled size
const resizeInputMaskToScaledSize = g.addNode({
id: getPrefixedId('resize_mask_to_scaled_size'),
type: 'img_resize',
...scaledSize,
});
g.addEdge(maskCombine, 'image', resizeInputMaskToScaledSize, 'image');
const alphaMaskToTensorMask = g.addNode({
type: 'image_mask_to_tensor',
id: getPrefixedId('image_mask_to_tensor'),
});
g.addEdge(resizeInputMaskToScaledSize, 'image', alphaMaskToTensorMask, 'image');
g.addEdge(alphaMaskToTensorMask, 'mask', fluxFill, 'mask');
// Resize the initial image to the scaled size and add to the FLUX Fill node
const resizeInputImageToScaledSize = g.addNode({
id: getPrefixedId('resize_image_to_scaled_size'),
type: 'img_resize',
image: { image_name: initialImage.image_name },
...scaledSize,
});
g.addEdge(resizeInputImageToScaledSize, 'image', fluxFill, 'image');
// Provide the FLUX Fill conditioning w/ image and mask to the denoise node
g.addEdge(fluxFill, 'fill_cond', denoise, 'fill_conditioning');
// Resize the output image back to the original size
const resizeOutputImageToOriginalSize = g.addNode({
id: getPrefixedId('resize_image_to_original_size'),
type: 'img_resize',
...originalSize,
});
// After denoising, resize the image and mask back to original size
g.addEdge(l2i, 'image', resizeOutputImageToOriginalSize, 'image');
const expandMask = g.addNode({
type: 'expand_mask_with_fade',
id: getPrefixedId('expand_mask_with_fade'),
fade_size_px: params.maskBlur,
});
g.addEdge(maskCombine, 'image', expandMask, 'mask');
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
// to canvas but not outputting only masked regions
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
layer_base: { image_name: initialImage.image_name },
});
g.addEdge(resizeOutputImageToOriginalSize, 'image', imageLayerBlend, 'layer_upper');
g.addEdge(expandMask, 'image', imageLayerBlend, 'mask');
return imageLayerBlend;
} else {
// Otherwise, just apply the mask
const applyMaskToImage = g.addNode({
type: 'apply_mask_to_image',
id: getPrefixedId('apply_mask_to_image'),
invert_mask: true,
});
g.addEdge(expandMask, 'image', applyMaskToImage, 'mask');
g.addEdge(resizeOutputImageToOriginalSize, 'image', applyMaskToImage, 'image');
return applyMaskToImage;
}
} else {
// Combine the inpaint mask and the initial image's alpha channel into a single mask
const maskAlphaToMask = g.addNode({
id: getPrefixedId('alpha_to_mask'),
type: 'tomask',
image: { image_name: maskImage.image_name },
invert: !canvasSettings.preserveMask,
});
const initialImageAlphaToMask = g.addNode({
id: getPrefixedId('image_alpha_to_mask'),
type: 'tomask',
image: { image_name: initialImage.image_name },
});
const maskCombine = g.addNode({
id: getPrefixedId('mask_combine'),
type: 'mask_combine',
});
g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1');
g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2');
const alphaMaskToTensorMask = g.addNode({
type: 'image_mask_to_tensor',
id: getPrefixedId('image_mask_to_tensor'),
});
g.addEdge(maskCombine, 'image', alphaMaskToTensorMask, 'image');
g.addEdge(alphaMaskToTensorMask, 'mask', fluxFill, 'mask');
fluxFill.image = { image_name: initialImage.image_name };
g.addEdge(fluxFill, 'fill_cond', denoise, 'fill_conditioning');
const expandMask = g.addNode({
type: 'expand_mask_with_fade',
id: getPrefixedId('expand_mask_with_fade'),
fade_size_px: params.maskBlur,
});
g.addEdge(maskCombine, 'image', expandMask, 'mask');
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
// to canvas but not outputting only masked regions
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
layer_base: { image_name: initialImage.image_name },
});
g.addEdge(l2i, 'image', imageLayerBlend, 'layer_upper');
g.addEdge(expandMask, 'image', imageLayerBlend, 'mask');
return imageLayerBlend;
} else {
// Otherwise, just apply the mask
const applyMaskToImage = g.addNode({
type: 'apply_mask_to_image',
id: getPrefixedId('apply_mask_to_image'),
invert_mask: true,
});
g.addEdge(expandMask, 'image', applyMaskToImage, 'mask');
g.addEdge(l2i, 'image', applyMaskToImage, 'image');
return applyMaskToImage;
}
}
};

View File

@@ -2,8 +2,7 @@ import type { CanvasReferenceImageState, FLUXReduxConfig } from 'features/contro
import { isFLUXReduxConfig } from 'features/controlLayers/store/types';
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
import type { Invocation } from 'services/api/types';
import type { Invocation, MainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
type AddFLUXReduxResult = {
@@ -14,7 +13,7 @@ type AddFLUXReduxArg = {
entities: CanvasReferenceImageState[];
g: Graph;
collector: Invocation<'collect'>;
model: ParameterModel;
model: MainModelConfig;
};
export const addFLUXReduxes = ({ entities, g, collector, model }: AddFLUXReduxArg): AddFLUXReduxResult => {

View File

@@ -5,8 +5,7 @@ import {
} from 'features/controlLayers/store/types';
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
import type { Invocation } from 'services/api/types';
import type { Invocation, MainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
type AddIPAdaptersResult = {
@@ -17,7 +16,7 @@ type AddIPAdaptersArg = {
entities: CanvasReferenceImageState[];
g: Graph;
collector: Invocation<'collect'>;
model: ParameterModel;
model: MainModelConfig;
};
export const addIPAdapters = ({ entities, g, collector, model }: AddIPAdaptersArg): AddIPAdaptersResult => {

View File

@@ -39,7 +39,7 @@ export const addInpaint = async ({
scaledSize,
denoising_start,
fp32,
}: AddInpaintArg): Promise<Invocation<'canvas_v2_mask_and_crop' | 'img_resize'>> => {
}: AddInpaintArg): Promise<Invocation<'invokeai_img_blend' | 'apply_mask_to_image'>> => {
denoise.denoising_start = denoising_start;
const params = selectParamsSlice(state);
@@ -104,10 +104,10 @@ export const addInpaint = async ({
edge_radius: params.canvasCoherenceEdgeSize,
fp32,
});
const canvasPasteBack = g.addNode({
id: getPrefixedId('canvas_v2_mask_and_crop'),
type: 'canvas_v2_mask_and_crop',
mask_blur: params.maskBlur,
const expandMask = g.addNode({
type: 'expand_mask_with_fade',
id: getPrefixedId('expand_mask_with_fade'),
fade_size_px: params.maskBlur,
});
// Resize initial image and mask to scaled size, feed into to gradient mask
@@ -128,18 +128,31 @@ export const addInpaint = async ({
// After denoising, resize the image and mask back to original size
g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image');
g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image');
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
// Finally, paste the generated masked image back onto the original image
g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
// After denoising, resize the image and mask back to original size
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
// to canvas but not outputting only masked regions
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
layer_base: { image_name: initialImage.image_name },
});
g.addEdge(resizeImageToOriginalSize, 'image', imageLayerBlend, 'layer_upper');
g.addEdge(resizeMaskToOriginalSize, 'image', imageLayerBlend, 'mask');
return imageLayerBlend;
} else {
// Otherwise, just apply the mask
const applyMaskToImage = g.addNode({
type: 'apply_mask_to_image',
id: getPrefixedId('apply_mask_to_image'),
invert_mask: true,
});
g.addEdge(resizeMaskToOriginalSize, 'image', applyMaskToImage, 'mask');
g.addEdge(resizeImageToOriginalSize, 'image', applyMaskToImage, 'image');
return applyMaskToImage;
}
return canvasPasteBack;
} else {
// No scale before processing, much simpler
const i2l = g.addNode({
@@ -164,11 +177,6 @@ export const addInpaint = async ({
fp32,
image: { image_name: initialImage.image_name },
});
const canvasPasteBack = g.addNode({
id: getPrefixedId('canvas_v2_mask_and_crop'),
type: 'canvas_v2_mask_and_crop',
mask_blur: params.maskBlur,
});
g.addEdge(alphaToMask, 'image', createGradientMask, 'mask');
g.addEdge(i2l, 'latents', denoise, 'latents');
@@ -178,16 +186,35 @@ export const addInpaint = async ({
g.addEdge(modelLoader, 'unet', createGradientMask, 'unet');
}
g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask');
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
const expandMask = g.addNode({
type: 'expand_mask_with_fade',
id: getPrefixedId('expand_mask_with_fade'),
fade_size_px: params.maskBlur,
});
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
// to canvas but not outputting only masked regions
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
layer_base: { image_name: initialImage.image_name },
});
g.addEdge(l2i, 'image', imageLayerBlend, 'layer_upper');
g.addEdge(expandMask, 'image', imageLayerBlend, 'mask');
return imageLayerBlend;
} else {
// Otherwise, just apply the mask
const applyMaskToImage = g.addNode({
type: 'apply_mask_to_image',
id: getPrefixedId('apply_mask_to_image'),
invert_mask: true,
});
g.addEdge(expandMask, 'image', applyMaskToImage, 'mask');
g.addEdge(l2i, 'image', applyMaskToImage, 'image');
return applyMaskToImage;
}
return canvasPasteBack;
}
};

View File

@@ -11,7 +11,14 @@ import type { Invocation } from 'services/api/types';
export const addNSFWChecker = (
g: Graph,
imageOutput: Invocation<
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
| 'l2i'
| 'img_nsfw'
| 'img_watermark'
| 'img_resize'
| 'invokeai_img_blend'
| 'apply_mask_to_image'
| 'flux_vae_decode'
| 'sd3_l2i'
>
): Invocation<'img_nsfw'> => {
const nsfw = g.addNode({

View File

@@ -40,7 +40,7 @@ export const addOutpaint = async ({
scaledSize,
denoising_start,
fp32,
}: AddOutpaintArg): Promise<Invocation<'canvas_v2_mask_and_crop' | 'img_resize'>> => {
}: AddOutpaintArg): Promise<Invocation<'invokeai_img_blend' | 'apply_mask_to_image'>> => {
denoise.denoising_start = denoising_start;
const params = selectParamsSlice(state);
@@ -142,29 +142,39 @@ export const addOutpaint = async ({
type: 'img_resize',
...originalSize,
});
const canvasPasteBack = g.addNode({
id: getPrefixedId('canvas_v2_mask_and_crop'),
type: 'canvas_v2_mask_and_crop',
mask_blur: params.maskBlur,
const expandMask = g.addNode({
type: 'expand_mask_with_fade',
id: getPrefixedId('expand_mask_with_fade'),
fade_size_px: params.maskBlur,
});
// Resize initial image and mask to scaled size, feed into to gradient mask
// After denoising, resize the image and mask back to original size
g.addEdge(l2i, 'image', resizeOutputImageToOriginalSize, 'image');
g.addEdge(createGradientMask, 'expanded_mask_area', resizeOutputMaskToOriginalSize, 'image');
// Finally, paste the generated masked image back onto the original image
g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
g.addEdge(expandMask, 'image', resizeOutputMaskToOriginalSize, 'image');
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
// to canvas but not outputting only masked regions
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
layer_base: { image_name: initialImage.image_name },
});
g.addEdge(resizeOutputImageToOriginalSize, 'image', imageLayerBlend, 'layer_upper');
g.addEdge(resizeOutputMaskToOriginalSize, 'image', imageLayerBlend, 'mask');
return imageLayerBlend;
} else {
// Otherwise, just apply the mask
const applyMaskToImage = g.addNode({
type: 'apply_mask_to_image',
id: getPrefixedId('apply_mask_to_image'),
invert_mask: true,
});
g.addEdge(resizeOutputMaskToOriginalSize, 'image', applyMaskToImage, 'mask');
g.addEdge(resizeOutputImageToOriginalSize, 'image', applyMaskToImage, 'image');
return applyMaskToImage;
}
return canvasPasteBack;
} else {
infill.image = { image_name: initialImage.image_name };
// No scale before processing, much simpler
@@ -197,11 +207,6 @@ export const addOutpaint = async ({
fp32,
image: { image_name: initialImage.image_name },
});
const canvasPasteBack = g.addNode({
id: getPrefixedId('canvas_v2_mask_and_crop'),
type: 'canvas_v2_mask_and_crop',
mask_blur: params.maskBlur,
});
g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1');
g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2');
g.addEdge(maskCombine, 'image', createGradientMask, 'mask');
@@ -214,15 +219,35 @@ export const addOutpaint = async ({
}
g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask');
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
const expandMask = g.addNode({
type: 'expand_mask_with_fade',
id: getPrefixedId('expand_mask_with_fade'),
fade_size_px: params.maskBlur,
});
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
// to canvas but not outputting only masked regions
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
const imageLayerBlend = g.addNode({
type: 'invokeai_img_blend',
id: getPrefixedId('image_layer_blend'),
layer_base: { image_name: initialImage.image_name },
});
g.addEdge(l2i, 'image', imageLayerBlend, 'layer_upper');
g.addEdge(expandMask, 'image', imageLayerBlend, 'mask');
return imageLayerBlend;
} else {
// Otherwise, just apply the mask
const applyMaskToImage = g.addNode({
type: 'apply_mask_to_image',
id: getPrefixedId('apply_mask_to_image'),
invert_mask: true,
});
g.addEdge(expandMask, 'image', applyMaskToImage, 'mask');
g.addEdge(l2i, 'image', applyMaskToImage, 'image');
return applyMaskToImage;
}
return canvasPasteBack;
}
};

View File

@@ -6,9 +6,8 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types';
import { getRegionalGuidanceWarnings } from 'features/controlLayers/store/validators';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
import { serializeError } from 'serialize-error';
import type { Invocation } from 'services/api/types';
import type { Invocation, MainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
const log = logger('system');
@@ -26,7 +25,7 @@ type AddRegionsArg = {
regions: CanvasRegionalGuidanceState[];
g: Graph;
bbox: Rect;
model: ParameterModel;
model: MainModelConfig;
posCond: Invocation<'compel' | 'sdxl_compel_prompt' | 'flux_text_encoder'>;
negCond: Invocation<'compel' | 'sdxl_compel_prompt' | 'flux_text_encoder'> | null;
posCondCollect: Invocation<'collect'>;

View File

@@ -11,7 +11,14 @@ import type { Invocation } from 'services/api/types';
export const addWatermarker = (
g: Graph,
imageOutput: Invocation<
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
| 'l2i'
| 'img_nsfw'
| 'img_watermark'
| 'img_resize'
| 'invokeai_img_blend'
| 'apply_mask_to_image'
| 'flux_vae_decode'
| 'sd3_l2i'
>
): Invocation<'img_watermark'> => {
const watermark = g.addNode({

View File

@@ -5,7 +5,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
import { addFLUXFill } from 'features/nodes/util/graph/generation/addFLUXFill';
import { addFLUXLoRAs } from 'features/nodes/util/graph/generation/addFLUXLoRAs';
import { addFLUXReduxes } from 'features/nodes/util/graph/generation/addFLUXRedux';
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
@@ -22,8 +22,9 @@ import {
getPresetModifiedPrompts,
getSizes,
} from 'features/nodes/util/graph/graphBuilderUtils';
import { t } from 'i18next';
import { selectMainModelConfig } from 'services/api/endpoints/models';
import type { Invocation } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
@@ -47,9 +48,10 @@ export const buildFLUXGraph = async (
const { originalSize, scaledSize } = getSizes(bbox);
const model = selectMainModelConfig(state);
const {
model,
guidance,
guidance: baseGuidance,
seed,
steps,
fluxVAE,
@@ -60,10 +62,34 @@ export const buildFLUXGraph = async (
} = params;
assert(model, 'No model found in state');
assert(model.base === 'flux', 'Model is not a FLUX model');
assert(t5EncoderModel, 'No T5 Encoder model found in state');
assert(clipEmbedModel, 'No CLIP Embed model found in state');
assert(fluxVAE, 'No FLUX VAE model found in state');
const isFLUXFill = model.variant === 'inpaint';
let guidance = baseGuidance;
if (isFLUXFill) {
// FLUX Fill doesn't work with Text to Image or Image to Image generation modes. Well, technically, it does, but
// the outputs are garbagio.
//
// Unfortunately, we do not know the generation mode until the user clicks Invoke, so this is the first place we
// can check for this incompatibility.
//
// We are opting to fail loudly instead of produce garbage images, hence this being an assert.
//
// The message in this assert will be shown in a toast to the user, so we are using the translation system for it.
//
// The other asserts above are just for sanity & type check and should never be hit, so they do not have
// translations.
assert(generationMode === 'inpaint' || generationMode === 'outpaint', t('toast.fluxFillIncompatibleWithT2IAndI2I'));
// FLUX Fill wants much higher guidance values than normal FLUX - silently "fix" the value for the user.
// TODO(psyche): Figure out a way to alert the user that this is happening - maybe return warnings from the graph
// builder and toast them?
guidance = 30;
}
const { positivePrompt } = getPresetModifiedPrompts(state);
const g = new Graph(getPrefixedId('flux_graph'));
@@ -115,16 +141,13 @@ export const buildFLUXGraph = async (
addFLUXLoRAs(state, g, denoise, modelLoader, posCond);
const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
assert(modelConfig.base === 'flux');
g.upsertMetadata({
generation_mode: 'flux_txt2img',
guidance,
width: originalSize.width,
height: originalSize.height,
positive_prompt: positivePrompt,
model: Graph.getModelMetadataField(modelConfig),
model: Graph.getModelMetadataField(model),
seed,
steps,
vae: fluxVAE,
@@ -143,10 +166,27 @@ export const buildFLUXGraph = async (
}
let canvasOutput: Invocation<
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
| 'l2i'
| 'img_nsfw'
| 'img_watermark'
| 'img_resize'
| 'invokeai_img_blend'
| 'apply_mask_to_image'
| 'flux_vae_decode'
| 'sd3_l2i'
> = l2i;
if (generationMode === 'txt2img') {
if (isFLUXFill) {
canvasOutput = await addFLUXFill({
state,
g,
manager,
l2i,
denoise,
originalSize,
scaledSize,
});
} else if (generationMode === 'txt2img') {
canvasOutput = addTextToImage({ g, l2i, originalSize, scaledSize });
} else if (generationMode === 'img2img') {
canvasOutput = await addImageToImage({
@@ -206,7 +246,7 @@ export const buildFLUXGraph = async (
g,
rect: canvas.bbox.rect,
collector: controlNetCollector,
model: modelConfig,
model,
});
if (controlNetResult.addedControlNets > 0) {
g.addEdge(controlNetCollector, 'collection', denoise, 'control');
@@ -220,7 +260,7 @@ export const buildFLUXGraph = async (
g,
rect: canvas.bbox.rect,
denoise,
model: modelConfig,
model,
});
const ipAdapterCollect = g.addNode({
@@ -231,7 +271,7 @@ export const buildFLUXGraph = async (
entities: canvas.referenceImages.entities,
g,
collector: ipAdapterCollect,
model: modelConfig,
model,
});
const fluxReduxCollect = g.addNode({
@@ -242,7 +282,7 @@ export const buildFLUXGraph = async (
entities: canvas.referenceImages.entities,
g,
collector: fluxReduxCollect,
model: modelConfig,
model,
});
const regionsResult = await addRegions({
@@ -250,7 +290,7 @@ export const buildFLUXGraph = async (
regions: canvas.regionalGuidance.entities,
g,
bbox: canvas.bbox.rect,
model: modelConfig,
model,
posCond,
negCond: null,
posCondCollect,

View File

@@ -5,7 +5,6 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters';
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint';
@@ -24,8 +23,8 @@ import {
getPresetModifiedPrompts,
getSizes,
} from 'features/nodes/util/graph/graphBuilderUtils';
import { selectMainModelConfig } from 'services/api/endpoints/models';
import type { Invocation } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
@@ -45,9 +44,9 @@ export const buildSD1Graph = async (
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
const model = selectMainModelConfig(state);
const {
model,
cfgScale: cfg_scale,
cfgRescaleMultiplier: cfg_rescale_multiplier,
scheduler,
@@ -137,8 +136,7 @@ export const buildSD1Graph = async (
g.addEdge(noise, 'noise', denoise, 'noise');
g.addEdge(denoise, 'latents', l2i, 'latents');
const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
assert(modelConfig.base === 'sd-1' || modelConfig.base === 'sd-2');
assert(model.base === 'sd-1' || model.base === 'sd-2');
g.upsertMetadata({
generation_mode: 'txt2img',
@@ -148,7 +146,7 @@ export const buildSD1Graph = async (
height: originalSize.height,
positive_prompt: positivePrompt,
negative_prompt: negativePrompt,
model: Graph.getModelMetadataField(modelConfig),
model: Graph.getModelMetadataField(model),
seed,
steps,
rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda',
@@ -170,7 +168,14 @@ export const buildSD1Graph = async (
const denoising_start = 1 - params.img2imgStrength;
let canvasOutput: Invocation<
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
| 'l2i'
| 'img_nsfw'
| 'img_watermark'
| 'img_resize'
| 'invokeai_img_blend'
| 'apply_mask_to_image'
| 'flux_vae_decode'
| 'sd3_l2i'
> = l2i;
if (generationMode === 'txt2img') {
@@ -233,7 +238,7 @@ export const buildSD1Graph = async (
g,
rect: canvas.bbox.rect,
collector: controlNetCollector,
model: modelConfig,
model,
});
if (controlNetResult.addedControlNets > 0) {
g.addEdge(controlNetCollector, 'collection', denoise, 'control');
@@ -251,7 +256,7 @@ export const buildSD1Graph = async (
g,
rect: canvas.bbox.rect,
collector: t2iAdapterCollector,
model: modelConfig,
model,
});
if (t2iAdapterResult.addedT2IAdapters > 0) {
g.addEdge(t2iAdapterCollector, 'collection', denoise, 't2i_adapter');
@@ -267,7 +272,7 @@ export const buildSD1Graph = async (
entities: canvas.referenceImages.entities,
g,
collector: ipAdapterCollect,
model: modelConfig,
model,
});
const regionsResult = await addRegions({
@@ -275,7 +280,7 @@ export const buildSD1Graph = async (
regions: canvas.regionalGuidance.entities,
g,
bbox: canvas.bbox.rect,
model: modelConfig,
model,
posCond,
negCond,
posCondCollect,

View File

@@ -5,7 +5,6 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint';
import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker';
@@ -19,8 +18,8 @@ import {
getPresetModifiedPrompts,
getSizes,
} from 'features/nodes/util/graph/graphBuilderUtils';
import { selectMainModelConfig } from 'services/api/endpoints/models';
import type { Invocation } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
@@ -33,6 +32,10 @@ export const buildSD3Graph = async (
const generationMode = await manager.compositor.getGenerationMode();
log.debug({ generationMode }, 'Building SD3 graph');
const model = selectMainModelConfig(state);
assert(model, 'No model found in state');
assert(model.base === 'sd-3');
const params = selectParamsSlice(state);
const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
@@ -40,7 +43,6 @@ export const buildSD3Graph = async (
const { bbox } = canvas;
const {
model,
cfgScale: cfg_scale,
seed,
steps,
@@ -52,8 +54,6 @@ export const buildSD3Graph = async (
img2imgStrength,
} = params;
assert(model, 'No model found in state');
const { originalSize, scaledSize } = getSizes(bbox);
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
@@ -107,9 +107,6 @@ export const buildSD3Graph = async (
g.addEdge(denoise, 'latents', l2i, 'latents');
const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
assert(modelConfig.base === 'sd-3');
g.upsertMetadata({
generation_mode: 'sd3_txt2img',
cfg_scale,
@@ -117,7 +114,7 @@ export const buildSD3Graph = async (
height: originalSize.height,
positive_prompt: positivePrompt,
negative_prompt: negativePrompt,
model: Graph.getModelMetadataField(modelConfig),
model: Graph.getModelMetadataField(model),
seed,
steps,
vae: vae ?? undefined,
@@ -135,7 +132,14 @@ export const buildSD3Graph = async (
}
let canvasOutput: Invocation<
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
| 'l2i'
| 'img_nsfw'
| 'img_watermark'
| 'img_resize'
| 'invokeai_img_blend'
| 'apply_mask_to_image'
| 'flux_vae_decode'
| 'sd3_l2i'
> = l2i;
if (generationMode === 'txt2img') {

View File

@@ -5,7 +5,6 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters';
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint';
@@ -24,8 +23,8 @@ import {
getPresetModifiedPrompts,
getSizes,
} from 'features/nodes/util/graph/graphBuilderUtils';
import { selectMainModelConfig } from 'services/api/endpoints/models';
import type { Invocation } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
@@ -40,6 +39,10 @@ export const buildSDXLGraph = async (
const generationMode = await manager.compositor.getGenerationMode();
log.debug({ generationMode }, 'Building SDXL graph');
const model = selectMainModelConfig(state);
assert(model, 'No model found in state');
assert(model.base === 'sdxl');
const params = selectParamsSlice(state);
const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
@@ -47,7 +50,6 @@ export const buildSDXLGraph = async (
const { bbox } = canvas;
const {
model,
cfgScale: cfg_scale,
cfgRescaleMultiplier: cfg_rescale_multiplier,
scheduler,
@@ -136,9 +138,6 @@ export const buildSDXLGraph = async (
g.addEdge(noise, 'noise', denoise, 'noise');
g.addEdge(denoise, 'latents', l2i, 'latents');
const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
assert(modelConfig.base === 'sdxl');
g.upsertMetadata({
generation_mode: 'sdxl_txt2img',
cfg_scale,
@@ -147,7 +146,7 @@ export const buildSDXLGraph = async (
height: originalSize.height,
positive_prompt: positivePrompt,
negative_prompt: negativePrompt,
model: Graph.getModelMetadataField(modelConfig),
model: Graph.getModelMetadataField(model),
seed,
steps,
rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda',
@@ -175,7 +174,14 @@ export const buildSDXLGraph = async (
: 1 - params.img2imgStrength;
let canvasOutput: Invocation<
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
| 'l2i'
| 'img_nsfw'
| 'img_watermark'
| 'img_resize'
| 'invokeai_img_blend'
| 'apply_mask_to_image'
| 'flux_vae_decode'
| 'sd3_l2i'
> = l2i;
if (generationMode === 'txt2img') {
@@ -238,7 +244,7 @@ export const buildSDXLGraph = async (
g,
rect: canvas.bbox.rect,
collector: controlNetCollector,
model: modelConfig,
model,
});
if (controlNetResult.addedControlNets > 0) {
g.addEdge(controlNetCollector, 'collection', denoise, 'control');
@@ -256,7 +262,7 @@ export const buildSDXLGraph = async (
g,
rect: canvas.bbox.rect,
collector: t2iAdapterCollector,
model: modelConfig,
model,
});
if (t2iAdapterResult.addedT2IAdapters > 0) {
g.addEdge(t2iAdapterCollector, 'collection', denoise, 't2i_adapter');
@@ -272,7 +278,7 @@ export const buildSDXLGraph = async (
entities: canvas.referenceImages.entities,
g,
collector: ipAdapterCollect,
model: modelConfig,
model,
});
const regionsResult = await addRegions({
@@ -280,7 +286,7 @@ export const buildSDXLGraph = async (
regions: canvas.regionalGuidance.entities,
g,
bbox: canvas.bbox.rect,
model: modelConfig,
model,
posCond,
negCond,
posCondCollect,

View File

@@ -39,6 +39,8 @@ import i18n from 'i18next';
import { debounce, groupBy, upperFirst } from 'lodash-es';
import { atom, computed } from 'nanostores';
import { useEffect } from 'react';
import { selectMainModelConfig } from 'services/api/endpoints/models';
import type { MainModelConfig } from 'services/api/types';
import { $isConnected } from 'services/events/stores';
/**
@@ -85,8 +87,10 @@ const debouncedUpdateReasons = debounce(
store: AppStore
) => {
if (tab === 'canvas') {
const model = selectMainModelConfig(store.getState());
const reasons = await getReasonsWhyCannotEnqueueCanvasTab({
isConnected,
model,
canvas,
params,
dynamicPrompts,
@@ -314,6 +318,7 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: {
const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
isConnected: boolean;
model: MainModelConfig | null | undefined;
canvas: CanvasState;
params: ParamsState;
dynamicPrompts: DynamicPromptsState;
@@ -325,6 +330,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
}) => {
const {
isConnected,
model,
canvas,
params,
dynamicPrompts,
@@ -334,7 +340,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
canvasIsCompositing,
canvasIsSelectingObject,
} = arg;
const { model, positivePrompt } = params;
const { positivePrompt } = params;
const reasons: Reason[] = [];
if (!isConnected) {

View File

@@ -22,6 +22,7 @@ import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
import { isFluxFillMainModelModelConfig } from 'services/api/types';
const formLabelProps: FormLabelProps = {
minW: '4rem',
@@ -83,7 +84,7 @@ export const GenerationSettingsAccordion = memo(() => {
{!isFLUX && !isSD3 && !isUpscaling && <ParamScheduler />}
{isUpscaling && <ParamUpscaleScheduler />}
<ParamSteps />
{isFLUX && <ParamGuidance />}
{isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && <ParamGuidance />}
{isUpscaling && <ParamUpscaleCFGScale />}
{!isFLUX && !isUpscaling && <ParamCFGScale />}
</FormControlGroup>

View File

@@ -1,9 +1,16 @@
import type { EntityState } from '@reduxjs/toolkit';
import { createEntityAdapter } from '@reduxjs/toolkit';
import { createEntityAdapter, createSelector } from '@reduxjs/toolkit';
import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import queryString from 'query-string';
import type { operations, paths } from 'services/api/schema';
import type { AnyModelConfig, GetHFTokenStatusResponse, SetHFTokenArg, SetHFTokenResponse } from 'services/api/types';
import {
type AnyModelConfig,
type GetHFTokenStatusResponse,
isNonRefinerMainModelConfig,
type SetHFTokenArg,
type SetHFTokenResponse,
} from 'services/api/types';
import type { Param0 } from 'tsafe';
import type { ApiTagDescription } from '..';
@@ -313,3 +320,23 @@ export const {
} = modelsApi;
export const selectModelConfigsQuery = modelsApi.endpoints.getModelConfigs.select();
export const selectMainModelConfig = createSelector(
selectModelConfigsQuery,
selectParamsSlice,
(modelConfigs, { model }) => {
if (!modelConfigs.data) {
return null;
}
if (!model) {
return null;
}
const modelConfig = modelConfigsAdapterSelectors.selectById(modelConfigs.data, model.key);
if (!modelConfig) {
return null;
}
if (!isNonRefinerMainModelConfig(modelConfig)) {
return null;
}
return modelConfig;
}
);

File diff suppressed because one or more lines are too long

View File

@@ -251,6 +251,10 @@ export const isFluxMainModelModelConfig = (config: AnyModelConfig): config is Ma
return config.type === 'main' && config.base === 'flux';
};
export const isFluxFillMainModelModelConfig = (config: AnyModelConfig): config is MainModelConfig => {
return config.type === 'main' && config.base === 'flux' && config.variant === 'inpaint';
};
export const isNonSDXLMainModelConfig = (config: AnyModelConfig): config is MainModelConfig => {
return config.type === 'main' && (config.base === 'sd-1' || config.base === 'sd-2');
};

View File

@@ -1 +1 @@
__version__ = "5.8.1"
__version__ = "5.9.0rc1"