Skip to content

Commit bb6c544

Browse files
feat(z-image): add Seed Variance Enhancer node and Linear UI integration (#8753)
* feat(z-image): add Seed Variance Enhancer node and Linear UI integration Add a new conditioning node for Z-Image models that injects seed-based noise into text embeddings to increase visual variation between seeds. Backend: - New invocation: z_image_seed_variance_enhancer.py - Parameters: strength (0-2), randomize_percent (1-100%), seed Frontend: - State management in paramsSlice with selectors and reducers - UI components in SeedVariance/ folder with toggle and sliders - Integration in GenerationSettingsAccordion (Advanced Options) - Graph builder integration in buildZImageGraph.ts - Metadata recall handlers for remix functionality - Translations and tooltip descriptions Based on: github.com/Pfannkuchensack/invokeai-z-image-seed-variance-enhancer * chore: ruff and typegen fix * chore: ruff and typegen fix * Revise seedVarianceStrength explanation Updated description for seedVarianceStrength. * Update description for seedVarianceStrength * fix(z-image): correct noise range comment from [-1, 1] to [-1, 1) torch.rand() generates [0, 1), so the scaled range excludes 1.
1 parent 8a18914 commit bb6c544

File tree

13 files changed

+497
-7
lines changed

13 files changed

+497
-7
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import torch
2+
3+
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
4+
from invokeai.app.invocations.fields import (
5+
FieldDescriptions,
6+
Input,
7+
InputField,
8+
ZImageConditioningField,
9+
)
10+
from invokeai.app.invocations.primitives import ZImageConditioningOutput
11+
from invokeai.app.services.shared.invocation_context import InvocationContext
12+
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
13+
ConditioningFieldData,
14+
ZImageConditioningInfo,
15+
)
16+
17+
18+
@invocation(
19+
"z_image_seed_variance_enhancer",
20+
title="Seed Variance Enhancer - Z-Image",
21+
tags=["conditioning", "z-image", "variance", "seed"],
22+
category="conditioning",
23+
version="1.0.0",
24+
classification=Classification.Prototype,
25+
)
26+
class ZImageSeedVarianceEnhancerInvocation(BaseInvocation):
27+
"""Adds seed-based noise to Z-Image conditioning to increase variance between seeds.
28+
29+
Z-Image-Turbo can produce relatively similar images with different seeds,
30+
making it harder to explore variations of a prompt. This node implements
31+
reproducible, seed-based noise injection into text embeddings to increase
32+
visual variation while maintaining reproducibility.
33+
34+
The noise strength is auto-calibrated relative to the embedding's standard
35+
deviation, ensuring consistent results across different prompts.
36+
"""
37+
38+
conditioning: ZImageConditioningField = InputField(
39+
description=FieldDescriptions.cond,
40+
input=Input.Connection,
41+
title="Conditioning",
42+
)
43+
seed: int = InputField(
44+
default=0,
45+
ge=0,
46+
description="Seed for reproducible noise generation. Different seeds produce different noise patterns.",
47+
)
48+
strength: float = InputField(
49+
default=0.1,
50+
ge=0.0,
51+
le=2.0,
52+
description="Noise strength as multiplier of embedding std. 0=off, 0.1=subtle, 0.5=strong.",
53+
)
54+
randomize_percent: float = InputField(
55+
default=50.0,
56+
ge=1.0,
57+
le=100.0,
58+
description="Percentage of embedding values to add noise to (1-100). Lower values create more selective noise patterns.",
59+
)
60+
61+
@torch.no_grad()
62+
def invoke(self, context: InvocationContext) -> ZImageConditioningOutput:
63+
# Load conditioning data
64+
cond_data = context.conditioning.load(self.conditioning.conditioning_name)
65+
assert len(cond_data.conditionings) == 1, "Expected exactly one conditioning tensor"
66+
z_image_conditioning = cond_data.conditionings[0]
67+
assert isinstance(z_image_conditioning, ZImageConditioningInfo), "Expected ZImageConditioningInfo"
68+
69+
# Early return if strength is zero (no modification needed)
70+
if self.strength == 0:
71+
return ZImageConditioningOutput(conditioning=self.conditioning)
72+
73+
# Clone embeddings to avoid modifying the original
74+
prompt_embeds = z_image_conditioning.prompt_embeds.clone()
75+
76+
# Calculate actual noise strength based on embedding statistics
77+
# This auto-calibration ensures consistent results across different prompts
78+
embed_std = torch.std(prompt_embeds).item()
79+
actual_strength = self.strength * embed_std
80+
81+
# Generate deterministic noise using the seed
82+
generator = torch.Generator(device=prompt_embeds.device)
83+
generator.manual_seed(self.seed)
84+
noise = torch.rand(
85+
prompt_embeds.shape, generator=generator, device=prompt_embeds.device, dtype=prompt_embeds.dtype
86+
)
87+
noise = noise * 2 - 1 # Scale to [-1, 1)
88+
noise = noise * actual_strength
89+
90+
# Create selective mask for noise application
91+
generator.manual_seed(self.seed + 1)
92+
noise_mask = torch.bernoulli(
93+
torch.ones_like(prompt_embeds) * (self.randomize_percent / 100.0),
94+
generator=generator,
95+
).bool()
96+
97+
# Apply noise only to masked positions
98+
prompt_embeds = prompt_embeds + (noise * noise_mask)
99+
100+
# Save modified conditioning
101+
new_conditioning = ZImageConditioningInfo(prompt_embeds=prompt_embeds)
102+
conditioning_data = ConditioningFieldData(conditionings=[new_conditioning])
103+
conditioning_name = context.conditioning.save(conditioning_data)
104+
105+
return ZImageConditioningOutput(
106+
conditioning=ZImageConditioningField(
107+
conditioning_name=conditioning_name,
108+
mask=self.conditioning.mask,
109+
)
110+
)

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,9 @@
856856
"scheduler": "Scheduler",
857857
"seamlessXAxis": "Seamless X Axis",
858858
"seamlessYAxis": "Seamless Y Axis",
859+
"seedVarianceEnabled": "Seed Variance Enabled",
860+
"seedVarianceStrength": "Seed Variance Strength",
861+
"seedVarianceRandomizePercent": "Seed Variance Randomize %",
859862
"seed": "Seed",
860863
"steps": "Steps",
861864
"strength": "Image to image strength",
@@ -1375,6 +1378,9 @@
13751378
"seamlessYAxis": "Seamless Y Axis",
13761379
"colorCompensation": "Color Compensation",
13771380
"seed": "Seed",
1381+
"seedVarianceEnabled": "Seed Variance Enhancer",
1382+
"seedVarianceStrength": "Variance Strength",
1383+
"seedVarianceRandomizePercent": "Randomize Percent",
13781384
"imageActions": "Image Actions",
13791385
"sendToCanvas": "Send To Canvas",
13801386
"sendToUpscale": "Send To Upscale",
@@ -1604,6 +1610,27 @@
16041610
"Each scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output."
16051611
]
16061612
},
1613+
"seedVarianceEnhancer": {
1614+
"heading": "Seed Variance Enhancer",
1615+
"paragraphs": [
1616+
"Z-Image-Turbo can produce relatively similar images with different seeds. This feature adds seed-based noise to the text embeddings to increase visual variation while maintaining reproducibility.",
1617+
"Enable this to get more diverse results when exploring different seeds."
1618+
]
1619+
},
1620+
"seedVarianceStrength": {
1621+
"heading": "Variance Strength",
1622+
"paragraphs": [
1623+
"Controls the intensity of the noise added to embeddings. The strength is automatically calibrated relative to the embedding's standard deviation.",
1624+
"Values less than 0.1 will produce subtle variations, increasing to stronger ones at 0.5. Values above 0.5 may lead to unexpected results."
1625+
]
1626+
},
1627+
"seedVarianceRandomizePercent": {
1628+
"heading": "Randomize Percent",
1629+
"paragraphs": [
1630+
"Percentage of embedding values that receive noise (1-100%).",
1631+
"Lower values create more selective noise patterns, while 100% affects all values equally."
1632+
]
1633+
},
16071634
"compositingMaskBlur": {
16081635
"heading": "Mask Blur",
16091636
"paragraphs": ["The blur radius of the mask."]

invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export type Feature =
77
| 'paramNegativeConditioning'
88
| 'paramPositiveConditioning'
99
| 'paramScheduler'
10+
| 'seedVarianceEnhancer'
11+
| 'seedVarianceStrength'
12+
| 'seedVarianceRandomizePercent'
1013
| 'compositingMaskBlur'
1114
| 'compositingBlurMethod'
1215
| 'compositingCoherencePass'

invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ const slice = createSlice({
7575
setZImageScheduler: (state, action: PayloadAction<'euler' | 'heun' | 'lcm'>) => {
7676
state.zImageScheduler = action.payload;
7777
},
78+
setZImageSeedVarianceEnabled: (state, action: PayloadAction<boolean>) => {
79+
state.zImageSeedVarianceEnabled = action.payload;
80+
},
81+
setZImageSeedVarianceStrength: (state, action: PayloadAction<number>) => {
82+
state.zImageSeedVarianceStrength = action.payload;
83+
},
84+
setZImageSeedVarianceRandomizePercent: (state, action: PayloadAction<number>) => {
85+
state.zImageSeedVarianceRandomizePercent = action.payload;
86+
},
7887
setUpscaleScheduler: (state, action: PayloadAction<ParameterScheduler>) => {
7988
state.upscaleScheduler = action.payload;
8089
},
@@ -457,6 +466,9 @@ export const {
457466
setScheduler,
458467
setFluxScheduler,
459468
setZImageScheduler,
469+
setZImageSeedVarianceEnabled,
470+
setZImageSeedVarianceStrength,
471+
setZImageSeedVarianceRandomizePercent,
460472
setUpscaleScheduler,
461473
setUpscaleCfgScale,
462474
setSeed,
@@ -598,6 +610,11 @@ export const selectModelSupportsOptimizedDenoising = createSelector(
598610
export const selectScheduler = createParamsSelector((params) => params.scheduler);
599611
export const selectFluxScheduler = createParamsSelector((params) => params.fluxScheduler);
600612
export const selectZImageScheduler = createParamsSelector((params) => params.zImageScheduler);
613+
export const selectZImageSeedVarianceEnabled = createParamsSelector((params) => params.zImageSeedVarianceEnabled);
614+
export const selectZImageSeedVarianceStrength = createParamsSelector((params) => params.zImageSeedVarianceStrength);
615+
export const selectZImageSeedVarianceRandomizePercent = createParamsSelector(
616+
(params) => params.zImageSeedVarianceRandomizePercent
617+
);
601618
export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis);
602619
export const selectSeamlessYAxis = createParamsSelector((params) => params.seamlessYAxis);
603620
export const selectSeed = createParamsSelector((params) => params.seed);

invokeai/frontend/web/src/features/controlLayers/store/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,10 @@ export const zParamsState = z.object({
633633
zImageVaeModel: zParameterVAEModel.nullable(), // Optional: Separate FLUX VAE
634634
zImageQwen3EncoderModel: zModelIdentifierField.nullable(), // Optional: Separate Qwen3 Encoder
635635
zImageQwen3SourceModel: zParameterModel.nullable(), // Diffusers Z-Image model (fallback for VAE/Encoder)
636+
// Z-Image Seed Variance Enhancer settings
637+
zImageSeedVarianceEnabled: z.boolean(),
638+
zImageSeedVarianceStrength: z.number().min(0).max(2),
639+
zImageSeedVarianceRandomizePercent: z.number().min(1).max(100),
636640
dimensions: zDimensionsState,
637641
});
638642
export type ParamsState = z.infer<typeof zParamsState>;
@@ -688,6 +692,9 @@ export const getInitialParamsState = (): ParamsState => ({
688692
zImageVaeModel: null,
689693
zImageQwen3EncoderModel: null,
690694
zImageQwen3SourceModel: null,
695+
zImageSeedVarianceEnabled: false,
696+
zImageSeedVarianceStrength: 0.1,
697+
zImageSeedVarianceRandomizePercent: 50,
691698
dimensions: {
692699
width: 512,
693700
height: 512,

invokeai/frontend/web/src/features/metadata/parsing.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import {
3131
setSeed,
3232
setSteps,
3333
setZImageScheduler,
34+
setZImageSeedVarianceEnabled,
35+
setZImageSeedVarianceRandomizePercent,
36+
setZImageSeedVarianceStrength,
3437
vaeSelected,
3538
widthChanged,
3639
zImageQwen3EncoderModelSelected,
@@ -534,6 +537,60 @@ const SeamlessY: SingleMetadataHandler<ParameterSeamlessY> = {
534537
};
535538
//#endregion SeamlessY
536539

540+
//#region ZImageSeedVarianceEnabled
541+
const ZImageSeedVarianceEnabled: SingleMetadataHandler<boolean> = {
542+
[SingleMetadataKey]: true,
543+
type: 'ZImageSeedVarianceEnabled',
544+
parse: (metadata, _store) => {
545+
const raw = getProperty(metadata, 'z_image_seed_variance_enabled');
546+
const parsed = z.boolean().parse(raw);
547+
return Promise.resolve(parsed);
548+
},
549+
recall: (value, store) => {
550+
store.dispatch(setZImageSeedVarianceEnabled(value));
551+
},
552+
i18nKey: 'metadata.seedVarianceEnabled',
553+
LabelComponent: MetadataLabel,
554+
ValueComponent: ({ value }: SingleMetadataValueProps<boolean>) => <MetadataPrimitiveValue value={value} />,
555+
};
556+
//#endregion ZImageSeedVarianceEnabled
557+
558+
//#region ZImageSeedVarianceStrength
559+
const ZImageSeedVarianceStrength: SingleMetadataHandler<number> = {
560+
[SingleMetadataKey]: true,
561+
type: 'ZImageSeedVarianceStrength',
562+
parse: (metadata, _store) => {
563+
const raw = getProperty(metadata, 'z_image_seed_variance_strength');
564+
const parsed = z.number().min(0).max(2).parse(raw);
565+
return Promise.resolve(parsed);
566+
},
567+
recall: (value, store) => {
568+
store.dispatch(setZImageSeedVarianceStrength(value));
569+
},
570+
i18nKey: 'metadata.seedVarianceStrength',
571+
LabelComponent: MetadataLabel,
572+
ValueComponent: ({ value }: SingleMetadataValueProps<number>) => <MetadataPrimitiveValue value={value} />,
573+
};
574+
//#endregion ZImageSeedVarianceStrength
575+
576+
//#region ZImageSeedVarianceRandomizePercent
577+
const ZImageSeedVarianceRandomizePercent: SingleMetadataHandler<number> = {
578+
[SingleMetadataKey]: true,
579+
type: 'ZImageSeedVarianceRandomizePercent',
580+
parse: (metadata, _store) => {
581+
const raw = getProperty(metadata, 'z_image_seed_variance_randomize_percent');
582+
const parsed = z.number().min(1).max(100).parse(raw);
583+
return Promise.resolve(parsed);
584+
},
585+
recall: (value, store) => {
586+
store.dispatch(setZImageSeedVarianceRandomizePercent(value));
587+
},
588+
i18nKey: 'metadata.seedVarianceRandomizePercent',
589+
LabelComponent: MetadataLabel,
590+
ValueComponent: ({ value }: SingleMetadataValueProps<number>) => <MetadataPrimitiveValue value={value} />,
591+
};
592+
//#endregion ZImageSeedVarianceRandomizePercent
593+
537594
//#region RefinerModel
538595
const RefinerModel: SingleMetadataHandler<ParameterSDXLRefinerModel> = {
539596
[SingleMetadataKey]: true,
@@ -1028,6 +1085,9 @@ export const ImageMetadataHandlers = {
10281085
Qwen3EncoderModel,
10291086
ZImageVAEModel,
10301087
ZImageQwen3SourceModel,
1088+
ZImageSeedVarianceEnabled,
1089+
ZImageSeedVarianceStrength,
1090+
ZImageSeedVarianceRandomizePercent,
10311091
LoRAs,
10321092
CanvasLayers,
10331093
RefImages,

invokeai/frontend/web/src/features/nodes/util/graph/generation/buildZImageGraph.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {
55
selectParamsSlice,
66
selectZImageQwen3EncoderModel,
77
selectZImageQwen3SourceModel,
8+
selectZImageSeedVarianceEnabled,
9+
selectZImageSeedVarianceRandomizePercent,
10+
selectZImageSeedVarianceStrength,
811
selectZImageVaeModel,
912
} from 'features/controlLayers/store/paramsSlice';
1013
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
@@ -55,6 +58,11 @@ export const buildZImageGraph = async (arg: GraphBuilderArg): Promise<GraphBuild
5558
// (1.0 means no CFG effect, matching FLUX convention)
5659
const { cfgScale: guidance_scale, steps, zImageScheduler } = params;
5760

61+
// Seed Variance Enhancer settings
62+
const seedVarianceEnabled = selectZImageSeedVarianceEnabled(state);
63+
const seedVarianceStrength = selectZImageSeedVarianceStrength(state);
64+
const seedVarianceRandomizePercent = selectZImageSeedVarianceRandomizePercent(state);
65+
5866
const prompts = selectPresetModifiedPrompts(state);
5967

6068
const g = new Graph(getPrefixedId('z_image_graph'));
@@ -127,8 +135,24 @@ export const buildZImageGraph = async (arg: GraphBuilderArg): Promise<GraphBuild
127135
g.addEdge(modelLoader, 'vae', denoise, 'vae');
128136

129137
g.addEdge(positivePrompt, 'value', posCond, 'prompt');
130-
// Connect positive conditioning through collector for regional support
131-
g.addEdge(posCond, 'conditioning', posCondCollect, 'item');
138+
139+
// Optionally add Seed Variance Enhancer between text encoder and denoise
140+
if (seedVarianceEnabled && seedVarianceStrength > 0) {
141+
const seedVarianceEnhancer = g.addNode({
142+
type: 'z_image_seed_variance_enhancer',
143+
id: getPrefixedId('seed_variance_enhancer'),
144+
strength: seedVarianceStrength,
145+
randomize_percent: seedVarianceRandomizePercent,
146+
});
147+
// Connect seed to variance enhancer
148+
g.addEdge(seed, 'value', seedVarianceEnhancer, 'seed');
149+
// Connect conditioning through the enhancer
150+
g.addEdge(posCond, 'conditioning', seedVarianceEnhancer, 'conditioning');
151+
g.addEdge(seedVarianceEnhancer, 'conditioning', posCondCollect, 'item');
152+
} else {
153+
// Connect positive conditioning directly through collector for regional support
154+
g.addEdge(posCond, 'conditioning', posCondCollect, 'item');
155+
}
132156
g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning');
133157

134158
// Connect negative conditioning if guidance_scale > 1
@@ -188,6 +212,10 @@ export const buildZImageGraph = async (arg: GraphBuilderArg): Promise<GraphBuild
188212
vae: zImageVaeModel ?? undefined,
189213
qwen3_encoder: zImageQwen3EncoderModel ?? undefined,
190214
qwen3_source: zImageQwen3SourceModel ?? undefined,
215+
// Seed Variance Enhancer settings
216+
z_image_seed_variance_enabled: seedVarianceEnabled,
217+
z_image_seed_variance_strength: seedVarianceStrength,
218+
z_image_seed_variance_randomize_percent: seedVarianceRandomizePercent,
191219
});
192220
g.addEdgeToMetadata(seed, 'value', 'seed');
193221
g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt');

0 commit comments

Comments
 (0)