Skip to content

Commit ed45bd4

Browse files
authored
Merge branch 'main' into copilot/enhancement-allow-shared-boards
2 parents ac1f1a5 + 60d0bcd commit ed45bd4

30 files changed

Lines changed: 2549 additions & 190 deletions

invokeai/app/invocations/canvas.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
2+
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField
3+
from invokeai.app.invocations.primitives import ImageOutput
4+
from invokeai.app.services.shared.invocation_context import InvocationContext
5+
6+
7+
@invocation(
8+
"canvas_output",
9+
title="Canvas Output",
10+
tags=["canvas", "output", "image"],
11+
category="canvas",
12+
version="1.0.0",
13+
use_cache=False,
14+
)
15+
class CanvasOutputInvocation(BaseInvocation):
16+
"""Outputs an image to the canvas staging area.
17+
18+
Use this node in workflows intended for canvas workflow integration.
19+
Connect the final image of your workflow to this node to send it
20+
to the canvas staging area when run via 'Run Workflow on Canvas'."""
21+
22+
image: ImageField = InputField(description=FieldDescriptions.image)
23+
24+
def invoke(self, context: InvocationContext) -> ImageOutput:
25+
image = context.images.get_pil(self.image.image_name)
26+
image_dto = context.images.save(image=image)
27+
return ImageOutput.build(image_dto)

invokeai/backend/model_manager/configs/main.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -323,16 +323,26 @@ def _is_flux2_model(state_dict: dict[str | int, Any]) -> bool:
323323
return False
324324

325325

326+
def _filename_suggests_base(name: str) -> bool:
327+
"""Check if a model name/filename suggests it is a Base (undistilled) variant.
328+
329+
Klein 9B Base and Klein 9B have identical architectures and cannot be distinguished
330+
from the state dict. We use the filename as a heuristic: filenames containing "base"
331+
(e.g. "flux-2-klein-base-9b", "FLUX.2-klein-base-9B") indicate the undistilled model.
332+
"""
333+
return "base" in name.lower()
334+
335+
326336
def _get_flux2_variant(state_dict: dict[str | int, Any]) -> Flux2VariantType | None:
327337
"""Determine FLUX.2 variant from state dict.
328338
329339
Distinguishes between Klein 4B and Klein 9B based on context embedding dimension:
330340
- Klein 4B: context_in_dim = 7680 (3 × Qwen3-4B hidden_size 2560)
331341
- Klein 9B: context_in_dim = 12288 (3 × Qwen3-8B hidden_size 4096)
332342
333-
Note: Klein 9B Base (undistilled) also has context_in_dim = 12288 but is rare.
334-
We default to Klein9B (distilled) for all 9B models since GGUF models may not
335-
include guidance embedding keys needed to distinguish them.
343+
Note: Klein 9B (distilled) and Klein 9B Base (undistilled) have identical architectures
344+
and cannot be distinguished from the state dict alone. This function defaults to Klein9B
345+
for all 9B models. Callers should use filename heuristics to detect Klein9BBase.
336346
337347
Supports both BFL format (checkpoint) and diffusers format keys:
338348
- BFL format: txt_in.weight (context embedder)
@@ -366,7 +376,7 @@ def _get_flux2_variant(state_dict: dict[str | int, Any]) -> Flux2VariantType | N
366376
context_in_dim = shape[1]
367377
# Determine variant based on context dimension
368378
if context_in_dim == KLEIN_9B_CONTEXT_DIM:
369-
# Default to Klein9B (distilled) - the official/common 9B model
379+
# Default to Klein9B - callers use filename heuristics to detect Klein9BBase
370380
return Flux2VariantType.Klein9B
371381
elif context_in_dim == KLEIN_4B_CONTEXT_DIM:
372382
return Flux2VariantType.Klein4B
@@ -553,6 +563,11 @@ def _get_variant_or_raise(cls, mod: ModelOnDisk) -> Flux2VariantType:
553563
if variant is None:
554564
raise NotAMatchError("unable to determine FLUX.2 model variant from state dict")
555565

566+
# Klein 9B Base and Klein 9B have identical architectures.
567+
# Use filename heuristic to detect the Base (undistilled) variant.
568+
if variant == Flux2VariantType.Klein9B and _filename_suggests_base(mod.name):
569+
return Flux2VariantType.Klein9BBase
570+
556571
return variant
557572

558573
@classmethod
@@ -720,6 +735,11 @@ def _get_variant_or_raise(cls, mod: ModelOnDisk) -> Flux2VariantType:
720735
if variant is None:
721736
raise NotAMatchError("unable to determine FLUX.2 model variant from state dict")
722737

738+
# Klein 9B Base and Klein 9B have identical architectures.
739+
# Use filename heuristic to detect the Base (undistilled) variant.
740+
if variant == Flux2VariantType.Klein9B and _filename_suggests_base(mod.name):
741+
return Flux2VariantType.Klein9BBase
742+
723743
return variant
724744

725745
@classmethod
@@ -829,30 +849,21 @@ def _get_variant_or_raise(cls, mod: ModelOnDisk) -> Flux2VariantType:
829849
- Klein 4B: joint_attention_dim = 7680 (3×Qwen3-4B hidden size)
830850
- Klein 9B/9B Base: joint_attention_dim = 12288 (3×Qwen3-8B hidden size)
831851
832-
To distinguish Klein 9B (distilled) from Klein 9B Base (undistilled),
833-
we check guidance_embeds:
834-
- Klein 9B (distilled): guidance_embeds = False (guidance is "baked in" during distillation)
835-
- Klein 9B Base (undistilled): guidance_embeds = True (needs guidance at inference)
836-
837-
Note: The official BFL Klein 9B model is the distilled version with guidance_embeds=False.
852+
Klein 9B (distilled) and Klein 9B Base (undistilled) have identical architectures
853+
and both have guidance_embeds=False. We use a filename heuristic to detect Base models.
838854
"""
839855
KLEIN_4B_CONTEXT_DIM = 7680 # 3 × 2560
840856
KLEIN_9B_CONTEXT_DIM = 12288 # 3 × 4096
841857

842858
transformer_config = get_config_dict_or_raise(mod.path / "transformer" / "config.json")
843859

844860
joint_attention_dim = transformer_config.get("joint_attention_dim", 4096)
845-
guidance_embeds = transformer_config.get("guidance_embeds", False)
846861

847862
# Determine variant based on joint_attention_dim
848863
if joint_attention_dim == KLEIN_9B_CONTEXT_DIM:
849-
# Check guidance_embeds to distinguish distilled from undistilled
850-
# Klein 9B (distilled): guidance_embeds = False (guidance is baked in)
851-
# Klein 9B Base (undistilled): guidance_embeds = True (needs guidance)
852-
if guidance_embeds:
864+
if _filename_suggests_base(mod.name):
853865
return Flux2VariantType.Klein9BBase
854-
else:
855-
return Flux2VariantType.Klein9B
866+
return Flux2VariantType.Klein9B
856867
elif joint_attention_dim == KLEIN_4B_CONTEXT_DIM:
857868
return Flux2VariantType.Klein4B
858869
elif joint_attention_dim > 4096:

invokeai/backend/model_manager/starter_models.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class StarterModelBundle(BaseModel):
7171
name="t5_base_encoder",
7272
base=BaseModelType.Any,
7373
source="InvokeAI/t5-v1_1-xxl::bfloat16",
74-
description="T5-XXL text encoder (used in FLUX pipelines). ~8GB",
74+
description="T5-XXL text encoder (used in FLUX pipelines). ~9.5GB",
7575
type=ModelType.T5Encoder,
7676
)
7777

@@ -156,39 +156,39 @@ class StarterModelBundle(BaseModel):
156156
name="FLUX.1 Kontext dev (quantized)",
157157
base=BaseModelType.Flux,
158158
source="https://huggingface.co/unsloth/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_M.gguf",
159-
description="FLUX.1 Kontext dev quantized (q4_k_m). Total size with dependencies: ~14GB",
159+
description="FLUX.1 Kontext dev quantized (q4_k_m). Total size with dependencies: ~12GB",
160160
type=ModelType.Main,
161161
dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder],
162162
)
163163
flux_krea = StarterModel(
164164
name="FLUX.1 Krea dev",
165165
base=BaseModelType.Flux,
166166
source="https://huggingface.co/InvokeAI/FLUX.1-Krea-dev/resolve/main/flux1-krea-dev.safetensors",
167-
description="FLUX.1 Krea dev. Total size with dependencies: ~33GB",
167+
description="FLUX.1 Krea dev. Total size with dependencies: ~29GB",
168168
type=ModelType.Main,
169169
dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder],
170170
)
171171
flux_krea_quantized = StarterModel(
172172
name="FLUX.1 Krea dev (quantized)",
173173
base=BaseModelType.Flux,
174174
source="https://huggingface.co/InvokeAI/FLUX.1-Krea-dev-GGUF/resolve/main/flux1-krea-dev-Q4_K_M.gguf",
175-
description="FLUX.1 Krea dev quantized (q4_k_m). Total size with dependencies: ~14GB",
175+
description="FLUX.1 Krea dev quantized (q4_k_m). Total size with dependencies: ~12GB",
176176
type=ModelType.Main,
177177
dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder],
178178
)
179179
sd35_medium = StarterModel(
180180
name="SD3.5 Medium",
181181
base=BaseModelType.StableDiffusion3,
182182
source="stabilityai/stable-diffusion-3.5-medium",
183-
description="Medium SD3.5 Model: ~15GB",
183+
description="Medium SD3.5 Model: ~16GB",
184184
type=ModelType.Main,
185185
dependencies=[],
186186
)
187187
sd35_large = StarterModel(
188188
name="SD3.5 Large",
189189
base=BaseModelType.StableDiffusion3,
190190
source="stabilityai/stable-diffusion-3.5-large",
191-
description="Large SD3.5 Model: ~19G",
191+
description="Large SD3.5 Model: ~28GB",
192192
type=ModelType.Main,
193193
dependencies=[],
194194
)
@@ -644,7 +644,7 @@ class StarterModelBundle(BaseModel):
644644
name="CogView4",
645645
base=BaseModelType.CogView4,
646646
source="THUDM/CogView4-6B",
647-
description="The base CogView4 model (~29GB).",
647+
description="The base CogView4 model (~31GB).",
648648
type=ModelType.Main,
649649
)
650650
# endregion
@@ -695,7 +695,7 @@ class StarterModelBundle(BaseModel):
695695
name="FLUX.2 VAE",
696696
base=BaseModelType.Flux2,
697697
source="black-forest-labs/FLUX.2-klein-4B::vae",
698-
description="FLUX.2 VAE (16-channel, same architecture as FLUX.1 VAE). ~335MB",
698+
description="FLUX.2 VAE (16-channel, same architecture as FLUX.1 VAE). ~168MB",
699699
type=ModelType.VAE,
700700
)
701701

@@ -719,7 +719,7 @@ class StarterModelBundle(BaseModel):
719719
name="FLUX.2 Klein 4B (Diffusers)",
720720
base=BaseModelType.Flux2,
721721
source="black-forest-labs/FLUX.2-klein-4B",
722-
description="FLUX.2 Klein 4B in Diffusers format - includes transformer, VAE and Qwen3 encoder. ~10GB",
722+
description="FLUX.2 Klein 4B in Diffusers format - includes transformer, VAE and Qwen3 encoder. ~16GB",
723723
type=ModelType.Main,
724724
)
725725

@@ -745,7 +745,7 @@ class StarterModelBundle(BaseModel):
745745
name="FLUX.2 Klein 9B (Diffusers)",
746746
base=BaseModelType.Flux2,
747747
source="black-forest-labs/FLUX.2-klein-9B",
748-
description="FLUX.2 Klein 9B in Diffusers format - includes transformer, VAE and Qwen3 encoder. ~20GB",
748+
description="FLUX.2 Klein 9B in Diffusers format - includes transformer, VAE and Qwen3 encoder. ~35GB",
749749
type=ModelType.Main,
750750
)
751751

@@ -821,7 +821,7 @@ class StarterModelBundle(BaseModel):
821821
name="Z-Image Turbo",
822822
base=BaseModelType.ZImage,
823823
source="Tongyi-MAI/Z-Image-Turbo",
824-
description="Z-Image Turbo - fast 6B parameter text-to-image model with 8 inference steps. Supports bilingual prompts (English & Chinese). ~30.6GB",
824+
description="Z-Image Turbo - fast 6B parameter text-to-image model with 8 inference steps. Supports bilingual prompts (English & Chinese). ~33GB",
825825
type=ModelType.Main,
826826
)
827827

invokeai/frontend/web/public/locales/en.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2391,6 +2391,27 @@
23912391
"pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage",
23922392
"addAdjustments": "Add Adjustments",
23932393
"removeAdjustments": "Remove Adjustments",
2394+
"workflowIntegration": {
2395+
"title": "Run Workflow on Canvas",
2396+
"description": "Select a workflow with a Canvas Output node and an image parameter to run on the current canvas layer. You can adjust parameters before executing. The result will be added back to the canvas.",
2397+
"execute": "Execute Workflow",
2398+
"executing": "Executing...",
2399+
"runWorkflow": "Run Workflow",
2400+
"filteringWorkflows": "Filtering workflows...",
2401+
"loadingWorkflows": "Loading workflows...",
2402+
"noWorkflowsFound": "No workflows found.",
2403+
"noWorkflowsWithImageField": "No compatible workflows found. A workflow needs a Form Builder with an image input field and a Canvas Output node.",
2404+
"selectWorkflow": "Select Workflow",
2405+
"selectPlaceholder": "Choose a workflow...",
2406+
"unnamedWorkflow": "Unnamed Workflow",
2407+
"loadingParameters": "Loading workflow parameters...",
2408+
"noFormBuilderError": "This workflow has no form builder and cannot be used. Please select a different workflow.",
2409+
"imageFieldSelected": "This field will receive the canvas image",
2410+
"imageFieldNotSelected": "Click to use this field for canvas image",
2411+
"executionStarted": "Workflow execution started",
2412+
"executionStartedDescription": "The result will appear in the staging area when complete.",
2413+
"executionFailed": "Failed to execute workflow"
2414+
},
23942415
"compositeOperation": {
23952416
"label": "Blend Mode",
23962417
"add": "Add Blend Mode",

invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys';
22
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
33
import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal';
4+
import { CanvasWorkflowIntegrationModal } from 'features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal';
45
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
56
import { CropImageModal } from 'features/cropper/components/CropImageModal';
67
import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal';
@@ -51,6 +52,7 @@ export const GlobalModalIsolator = memo(() => {
5152
<SaveWorkflowAsDialog />
5253
<CanvasManagerProviderGate>
5354
<CanvasPasteModal />
55+
<CanvasWorkflowIntegrationModal />
5456
</CanvasManagerProviderGate>
5557
<LoadWorkflowFromGraphModal />
5658
<CropImageModal />

invokeai/frontend/web/src/app/logging/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));
1616

1717
export const zLogNamespace = z.enum([
1818
'canvas',
19+
'canvas-workflow-integration',
1920
'config',
2021
'dnd',
2122
'events',

invokeai/frontend/web/src/app/store/store.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSe
2525
import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice';
2626
import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice';
2727
import { canvasTextSliceConfig } from 'features/controlLayers/store/canvasTextSlice';
28+
import { canvasWorkflowIntegrationSliceConfig } from 'features/controlLayers/store/canvasWorkflowIntegrationSlice';
2829
import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice';
2930
import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice';
3031
import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice';
@@ -67,6 +68,7 @@ const SLICE_CONFIGS = {
6768
[canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig,
6869
[canvasTextSliceConfig.slice.reducerPath]: canvasTextSliceConfig,
6970
[canvasSliceConfig.slice.reducerPath]: canvasSliceConfig,
71+
[canvasWorkflowIntegrationSliceConfig.slice.reducerPath]: canvasWorkflowIntegrationSliceConfig,
7072
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig,
7173
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig,
7274
[gallerySliceConfig.slice.reducerPath]: gallerySliceConfig,
@@ -98,6 +100,7 @@ const ALL_REDUCERS = {
98100
canvasSliceConfig.slice.reducer,
99101
canvasSliceConfig.undoableConfig?.reduxUndoOptions
100102
),
103+
[canvasWorkflowIntegrationSliceConfig.slice.reducerPath]: canvasWorkflowIntegrationSliceConfig.slice.reducer,
101104
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer,
102105
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer,
103106
[gallerySliceConfig.slice.reducerPath]: gallerySliceConfig.slice.reducer,
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {
2+
Button,
3+
ButtonGroup,
4+
Flex,
5+
Heading,
6+
Modal,
7+
ModalBody,
8+
ModalCloseButton,
9+
ModalContent,
10+
ModalFooter,
11+
ModalHeader,
12+
ModalOverlay,
13+
Spacer,
14+
Spinner,
15+
Text,
16+
} from '@invoke-ai/ui-library';
17+
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
18+
import {
19+
canvasWorkflowIntegrationClosed,
20+
selectCanvasWorkflowIntegrationIsOpen,
21+
selectCanvasWorkflowIntegrationIsProcessing,
22+
selectCanvasWorkflowIntegrationSelectedWorkflowId,
23+
} from 'features/controlLayers/store/canvasWorkflowIntegrationSlice';
24+
import { memo, useCallback } from 'react';
25+
import { useTranslation } from 'react-i18next';
26+
27+
import { CanvasWorkflowIntegrationParameterPanel } from './CanvasWorkflowIntegrationParameterPanel';
28+
import { CanvasWorkflowIntegrationWorkflowSelector } from './CanvasWorkflowIntegrationWorkflowSelector';
29+
import { useCanvasWorkflowIntegrationExecute } from './useCanvasWorkflowIntegrationExecute';
30+
31+
export const CanvasWorkflowIntegrationModal = memo(() => {
32+
const { t } = useTranslation();
33+
const dispatch = useAppDispatch();
34+
35+
const isOpen = useAppSelector(selectCanvasWorkflowIntegrationIsOpen);
36+
const isProcessing = useAppSelector(selectCanvasWorkflowIntegrationIsProcessing);
37+
const selectedWorkflowId = useAppSelector(selectCanvasWorkflowIntegrationSelectedWorkflowId);
38+
39+
const { execute, canExecute } = useCanvasWorkflowIntegrationExecute();
40+
41+
const onClose = useCallback(() => {
42+
if (!isProcessing) {
43+
dispatch(canvasWorkflowIntegrationClosed());
44+
}
45+
}, [dispatch, isProcessing]);
46+
47+
const onExecute = useCallback(() => {
48+
execute();
49+
}, [execute]);
50+
51+
return (
52+
<Modal isOpen={isOpen} onClose={onClose} size="xl" isCentered>
53+
<ModalOverlay />
54+
<ModalContent>
55+
<ModalHeader>
56+
<Heading size="md">{t('controlLayers.workflowIntegration.title')}</Heading>
57+
</ModalHeader>
58+
<ModalCloseButton isDisabled={isProcessing} />
59+
60+
<ModalBody>
61+
<Flex direction="column" gap={4}>
62+
<Text fontSize="sm" color="base.400">
63+
{t('controlLayers.workflowIntegration.description')}
64+
</Text>
65+
66+
<CanvasWorkflowIntegrationWorkflowSelector />
67+
68+
{selectedWorkflowId && <CanvasWorkflowIntegrationParameterPanel />}
69+
</Flex>
70+
</ModalBody>
71+
72+
<ModalFooter>
73+
<ButtonGroup>
74+
<Button variant="ghost" onClick={onClose} isDisabled={isProcessing}>
75+
{t('common.cancel')}
76+
</Button>
77+
<Spacer />
78+
<Button
79+
onClick={onExecute}
80+
isDisabled={!canExecute || isProcessing}
81+
loadingText={t('controlLayers.workflowIntegration.executing')}
82+
>
83+
{isProcessing && <Spinner size="sm" mr={2} />}
84+
{t('controlLayers.workflowIntegration.execute')}
85+
</Button>
86+
</ButtonGroup>
87+
</ModalFooter>
88+
</ModalContent>
89+
</Modal>
90+
);
91+
});
92+
93+
CanvasWorkflowIntegrationModal.displayName = 'CanvasWorkflowIntegrationModal';

0 commit comments

Comments
 (0)