Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions invokeai/app/invocations/z_image_seed_variance_enhancer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import torch

from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.fields import (
FieldDescriptions,
Input,
InputField,
ZImageConditioningField,
)
from invokeai.app.invocations.primitives import ZImageConditioningOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
ConditioningFieldData,
ZImageConditioningInfo,
)


@invocation(
"z_image_seed_variance_enhancer",
title="Seed Variance Enhancer - Z-Image",
tags=["conditioning", "z-image", "variance", "seed"],
category="conditioning",
version="1.0.0",
classification=Classification.Prototype,
)
class ZImageSeedVarianceEnhancerInvocation(BaseInvocation):
"""Adds seed-based noise to Z-Image conditioning to increase variance between seeds.

Z-Image-Turbo can produce relatively similar images with different seeds,
making it harder to explore variations of a prompt. This node implements
reproducible, seed-based noise injection into text embeddings to increase
visual variation while maintaining reproducibility.

The noise strength is auto-calibrated relative to the embedding's standard
deviation, ensuring consistent results across different prompts.
"""

conditioning: ZImageConditioningField = InputField(
description=FieldDescriptions.cond,
input=Input.Connection,
title="Conditioning",
)
seed: int = InputField(
default=0,
ge=0,
description="Seed for reproducible noise generation. Different seeds produce different noise patterns.",
)
strength: float = InputField(
default=0.1,
ge=0.0,
le=2.0,
description="Noise strength as multiplier of embedding std. 0=off, 0.1=subtle, 0.5=strong.",
)
randomize_percent: float = InputField(
default=50.0,
ge=1.0,
le=100.0,
description="Percentage of embedding values to add noise to (1-100). Lower values create more selective noise patterns.",
)

@torch.no_grad()
def invoke(self, context: InvocationContext) -> ZImageConditioningOutput:
# Load conditioning data
cond_data = context.conditioning.load(self.conditioning.conditioning_name)
assert len(cond_data.conditionings) == 1, "Expected exactly one conditioning tensor"
z_image_conditioning = cond_data.conditionings[0]
assert isinstance(z_image_conditioning, ZImageConditioningInfo), "Expected ZImageConditioningInfo"

# Early return if strength is zero (no modification needed)
if self.strength == 0:
return ZImageConditioningOutput(conditioning=self.conditioning)

# Clone embeddings to avoid modifying the original
prompt_embeds = z_image_conditioning.prompt_embeds.clone()

# Calculate actual noise strength based on embedding statistics
# This auto-calibration ensures consistent results across different prompts
embed_std = torch.std(prompt_embeds).item()
actual_strength = self.strength * embed_std

# Generate deterministic noise using the seed
generator = torch.Generator(device=prompt_embeds.device)
generator.manual_seed(self.seed)
noise = torch.rand(
prompt_embeds.shape, generator=generator, device=prompt_embeds.device, dtype=prompt_embeds.dtype
)
noise = noise * 2 - 1 # Scale to [-1, 1)
noise = noise * actual_strength

# Create selective mask for noise application
generator.manual_seed(self.seed + 1)
noise_mask = torch.bernoulli(
torch.ones_like(prompt_embeds) * (self.randomize_percent / 100.0),
generator=generator,
).bool()

# Apply noise only to masked positions
prompt_embeds = prompt_embeds + (noise * noise_mask)

# Save modified conditioning
new_conditioning = ZImageConditioningInfo(prompt_embeds=prompt_embeds)
conditioning_data = ConditioningFieldData(conditionings=[new_conditioning])
conditioning_name = context.conditioning.save(conditioning_data)

return ZImageConditioningOutput(
conditioning=ZImageConditioningField(
conditioning_name=conditioning_name,
mask=self.conditioning.mask,
)
)
27 changes: 27 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,9 @@
"scheduler": "Scheduler",
"seamlessXAxis": "Seamless X Axis",
"seamlessYAxis": "Seamless Y Axis",
"seedVarianceEnabled": "Seed Variance Enabled",
"seedVarianceStrength": "Seed Variance Strength",
"seedVarianceRandomizePercent": "Seed Variance Randomize %",
"seed": "Seed",
"steps": "Steps",
"strength": "Image to image strength",
Expand Down Expand Up @@ -1375,6 +1378,9 @@
"seamlessYAxis": "Seamless Y Axis",
"colorCompensation": "Color Compensation",
"seed": "Seed",
"seedVarianceEnabled": "Seed Variance Enhancer",
"seedVarianceStrength": "Variance Strength",
"seedVarianceRandomizePercent": "Randomize Percent",
"imageActions": "Image Actions",
"sendToCanvas": "Send To Canvas",
"sendToUpscale": "Send To Upscale",
Expand Down Expand Up @@ -1604,6 +1610,27 @@
"Each scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output."
]
},
"seedVarianceEnhancer": {
"heading": "Seed Variance Enhancer",
"paragraphs": [
"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.",
"Enable this to get more diverse results when exploring different seeds."
]
},
"seedVarianceStrength": {
"heading": "Variance Strength",
"paragraphs": [
"Controls the intensity of the noise added to embeddings. The strength is automatically calibrated relative to the embedding's standard deviation.",
"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."
]
},
"seedVarianceRandomizePercent": {
"heading": "Randomize Percent",
"paragraphs": [
"Percentage of embedding values that receive noise (1-100%).",
"Lower values create more selective noise patterns, while 100% affects all values equally."
]
},
"compositingMaskBlur": {
"heading": "Mask Blur",
"paragraphs": ["The blur radius of the mask."]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export type Feature =
| 'paramNegativeConditioning'
| 'paramPositiveConditioning'
| 'paramScheduler'
| 'seedVarianceEnhancer'
| 'seedVarianceStrength'
| 'seedVarianceRandomizePercent'
| 'compositingMaskBlur'
| 'compositingBlurMethod'
| 'compositingCoherencePass'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ const slice = createSlice({
setZImageScheduler: (state, action: PayloadAction<'euler' | 'heun' | 'lcm'>) => {
state.zImageScheduler = action.payload;
},
setZImageSeedVarianceEnabled: (state, action: PayloadAction<boolean>) => {
state.zImageSeedVarianceEnabled = action.payload;
},
setZImageSeedVarianceStrength: (state, action: PayloadAction<number>) => {
state.zImageSeedVarianceStrength = action.payload;
},
setZImageSeedVarianceRandomizePercent: (state, action: PayloadAction<number>) => {
state.zImageSeedVarianceRandomizePercent = action.payload;
},
setUpscaleScheduler: (state, action: PayloadAction<ParameterScheduler>) => {
state.upscaleScheduler = action.payload;
},
Expand Down Expand Up @@ -457,6 +466,9 @@ export const {
setScheduler,
setFluxScheduler,
setZImageScheduler,
setZImageSeedVarianceEnabled,
setZImageSeedVarianceStrength,
setZImageSeedVarianceRandomizePercent,
setUpscaleScheduler,
setUpscaleCfgScale,
setSeed,
Expand Down Expand Up @@ -598,6 +610,11 @@ export const selectModelSupportsOptimizedDenoising = createSelector(
export const selectScheduler = createParamsSelector((params) => params.scheduler);
export const selectFluxScheduler = createParamsSelector((params) => params.fluxScheduler);
export const selectZImageScheduler = createParamsSelector((params) => params.zImageScheduler);
export const selectZImageSeedVarianceEnabled = createParamsSelector((params) => params.zImageSeedVarianceEnabled);
export const selectZImageSeedVarianceStrength = createParamsSelector((params) => params.zImageSeedVarianceStrength);
export const selectZImageSeedVarianceRandomizePercent = createParamsSelector(
(params) => params.zImageSeedVarianceRandomizePercent
);
export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis);
export const selectSeamlessYAxis = createParamsSelector((params) => params.seamlessYAxis);
export const selectSeed = createParamsSelector((params) => params.seed);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,10 @@ export const zParamsState = z.object({
zImageVaeModel: zParameterVAEModel.nullable(), // Optional: Separate FLUX VAE
zImageQwen3EncoderModel: zModelIdentifierField.nullable(), // Optional: Separate Qwen3 Encoder
zImageQwen3SourceModel: zParameterModel.nullable(), // Diffusers Z-Image model (fallback for VAE/Encoder)
// Z-Image Seed Variance Enhancer settings
zImageSeedVarianceEnabled: z.boolean(),
zImageSeedVarianceStrength: z.number().min(0).max(2),
zImageSeedVarianceRandomizePercent: z.number().min(1).max(100),
dimensions: zDimensionsState,
});
export type ParamsState = z.infer<typeof zParamsState>;
Expand Down Expand Up @@ -688,6 +692,9 @@ export const getInitialParamsState = (): ParamsState => ({
zImageVaeModel: null,
zImageQwen3EncoderModel: null,
zImageQwen3SourceModel: null,
zImageSeedVarianceEnabled: false,
zImageSeedVarianceStrength: 0.1,
zImageSeedVarianceRandomizePercent: 50,
dimensions: {
width: 512,
height: 512,
Expand Down
60 changes: 60 additions & 0 deletions invokeai/frontend/web/src/features/metadata/parsing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import {
setSeed,
setSteps,
setZImageScheduler,
setZImageSeedVarianceEnabled,
setZImageSeedVarianceRandomizePercent,
setZImageSeedVarianceStrength,
vaeSelected,
widthChanged,
zImageQwen3EncoderModelSelected,
Expand Down Expand Up @@ -534,6 +537,60 @@ const SeamlessY: SingleMetadataHandler<ParameterSeamlessY> = {
};
//#endregion SeamlessY

//#region ZImageSeedVarianceEnabled
const ZImageSeedVarianceEnabled: SingleMetadataHandler<boolean> = {
[SingleMetadataKey]: true,
type: 'ZImageSeedVarianceEnabled',
parse: (metadata, _store) => {
const raw = getProperty(metadata, 'z_image_seed_variance_enabled');
const parsed = z.boolean().parse(raw);
return Promise.resolve(parsed);
},
recall: (value, store) => {
store.dispatch(setZImageSeedVarianceEnabled(value));
},
i18nKey: 'metadata.seedVarianceEnabled',
LabelComponent: MetadataLabel,
ValueComponent: ({ value }: SingleMetadataValueProps<boolean>) => <MetadataPrimitiveValue value={value} />,
};
//#endregion ZImageSeedVarianceEnabled

//#region ZImageSeedVarianceStrength
const ZImageSeedVarianceStrength: SingleMetadataHandler<number> = {
[SingleMetadataKey]: true,
type: 'ZImageSeedVarianceStrength',
parse: (metadata, _store) => {
const raw = getProperty(metadata, 'z_image_seed_variance_strength');
const parsed = z.number().min(0).max(2).parse(raw);
return Promise.resolve(parsed);
},
recall: (value, store) => {
store.dispatch(setZImageSeedVarianceStrength(value));
},
i18nKey: 'metadata.seedVarianceStrength',
LabelComponent: MetadataLabel,
ValueComponent: ({ value }: SingleMetadataValueProps<number>) => <MetadataPrimitiveValue value={value} />,
};
//#endregion ZImageSeedVarianceStrength

//#region ZImageSeedVarianceRandomizePercent
const ZImageSeedVarianceRandomizePercent: SingleMetadataHandler<number> = {
[SingleMetadataKey]: true,
type: 'ZImageSeedVarianceRandomizePercent',
parse: (metadata, _store) => {
const raw = getProperty(metadata, 'z_image_seed_variance_randomize_percent');
const parsed = z.number().min(1).max(100).parse(raw);
return Promise.resolve(parsed);
},
recall: (value, store) => {
store.dispatch(setZImageSeedVarianceRandomizePercent(value));
},
i18nKey: 'metadata.seedVarianceRandomizePercent',
LabelComponent: MetadataLabel,
ValueComponent: ({ value }: SingleMetadataValueProps<number>) => <MetadataPrimitiveValue value={value} />,
};
//#endregion ZImageSeedVarianceRandomizePercent

//#region RefinerModel
const RefinerModel: SingleMetadataHandler<ParameterSDXLRefinerModel> = {
[SingleMetadataKey]: true,
Expand Down Expand Up @@ -1028,6 +1085,9 @@ export const ImageMetadataHandlers = {
Qwen3EncoderModel,
ZImageVAEModel,
ZImageQwen3SourceModel,
ZImageSeedVarianceEnabled,
ZImageSeedVarianceStrength,
ZImageSeedVarianceRandomizePercent,
LoRAs,
CanvasLayers,
RefImages,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
selectParamsSlice,
selectZImageQwen3EncoderModel,
selectZImageQwen3SourceModel,
selectZImageSeedVarianceEnabled,
selectZImageSeedVarianceRandomizePercent,
selectZImageSeedVarianceStrength,
selectZImageVaeModel,
} from 'features/controlLayers/store/paramsSlice';
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
Expand Down Expand Up @@ -55,6 +58,11 @@ export const buildZImageGraph = async (arg: GraphBuilderArg): Promise<GraphBuild
// (1.0 means no CFG effect, matching FLUX convention)
const { cfgScale: guidance_scale, steps, zImageScheduler } = params;

// Seed Variance Enhancer settings
const seedVarianceEnabled = selectZImageSeedVarianceEnabled(state);
const seedVarianceStrength = selectZImageSeedVarianceStrength(state);
const seedVarianceRandomizePercent = selectZImageSeedVarianceRandomizePercent(state);

const prompts = selectPresetModifiedPrompts(state);

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

g.addEdge(positivePrompt, 'value', posCond, 'prompt');
// Connect positive conditioning through collector for regional support
g.addEdge(posCond, 'conditioning', posCondCollect, 'item');

// Optionally add Seed Variance Enhancer between text encoder and denoise
if (seedVarianceEnabled && seedVarianceStrength > 0) {
const seedVarianceEnhancer = g.addNode({
type: 'z_image_seed_variance_enhancer',
id: getPrefixedId('seed_variance_enhancer'),
strength: seedVarianceStrength,
randomize_percent: seedVarianceRandomizePercent,
});
// Connect seed to variance enhancer
g.addEdge(seed, 'value', seedVarianceEnhancer, 'seed');
// Connect conditioning through the enhancer
g.addEdge(posCond, 'conditioning', seedVarianceEnhancer, 'conditioning');
g.addEdge(seedVarianceEnhancer, 'conditioning', posCondCollect, 'item');
} else {
// Connect positive conditioning directly through collector for regional support
g.addEdge(posCond, 'conditioning', posCondCollect, 'item');
}
g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning');

// Connect negative conditioning if guidance_scale > 1
Expand Down Expand Up @@ -188,6 +212,10 @@ export const buildZImageGraph = async (arg: GraphBuilderArg): Promise<GraphBuild
vae: zImageVaeModel ?? undefined,
qwen3_encoder: zImageQwen3EncoderModel ?? undefined,
qwen3_source: zImageQwen3SourceModel ?? undefined,
// Seed Variance Enhancer settings
z_image_seed_variance_enabled: seedVarianceEnabled,
z_image_seed_variance_strength: seedVarianceStrength,
z_image_seed_variance_randomize_percent: seedVarianceRandomizePercent,
});
g.addEdgeToMetadata(seed, 'value', 'seed');
g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt');
Expand Down
Loading