Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
99fc124
feat(flux): add scheduler selection for Flux models
Pfannkuchensack Dec 26, 2025
56bef0b
feat(z-image): add scheduler selection for Z-Image models
Pfannkuchensack Dec 26, 2025
bd678b1
fix ruff check
Pfannkuchensack Dec 26, 2025
eb516e1
Merge branch 'main' into feature/zimage-scheduler-support
JPPhoto Dec 27, 2025
0956ce0
Merge branch 'main' into feature/zimage-scheduler-support
Pfannkuchensack Dec 27, 2025
b4f05d3
Merge branch 'main' into feature/zimage-scheduler-support
lstein Dec 28, 2025
16fedfb
fix(schedulers): prevent progress percentage overflow with LCM scheduler
Pfannkuchensack Dec 28, 2025
bc47830
Merge branch 'main' into feature/zimage-scheduler-support
Pfannkuchensack Dec 28, 2025
9617140
Merge branch 'feature/zimage-scheduler-support' of https://github.com…
Pfannkuchensack Dec 28, 2025
0f830dd
Ruff format
Pfannkuchensack Dec 28, 2025
8d880ef
fix(schedulers): remove initial step-0 callback for consistent step c…
Pfannkuchensack Dec 29, 2025
6c3ce8e
Merge branch 'main' into feature/zimage-scheduler-support
lstein Jan 2, 2026
e7233ef
Merge branch 'main' into feature/zimage-scheduler-support
JPPhoto Jan 2, 2026
132a484
feat(z-image): add scheduler support with metadata recall
Pfannkuchensack Jan 3, 2026
1bcf589
feat(z-image): add Z-Image Denoise + Metadata node
Pfannkuchensack Jan 3, 2026
7847cce
fix typegen
Pfannkuchensack Jan 3, 2026
252794d
ruff fix
Pfannkuchensack Jan 3, 2026
d99707f
fix(ui): fix z-image scheduler recall by reordering metadata handlers
Pfannkuchensack Jan 3, 2026
81d83d5
Merge branch 'main' into z-image_metadata_node
Pfannkuchensack Jan 3, 2026
2ccadd1
Merge branch 'main' into z-image_metadata_node
lstein Jan 4, 2026
1ca589e
Merge branch 'main' into z-image_metadata_node
Pfannkuchensack Jan 4, 2026
f29820a
feat(ui): improve Z-Image model selector UX with auto-clearing conflicts
Pfannkuchensack Jan 4, 2026
a05a626
Fix typegen
Pfannkuchensack Jan 5, 2026
384a1a6
Merge branch 'main' into z-image_metadata_node
Pfannkuchensack Jan 5, 2026
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
47 changes: 47 additions & 0 deletions invokeai/app/invocations/metadata_linked.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
)
from invokeai.app.invocations.scheduler import SchedulerOutput
from invokeai.app.invocations.t2i_adapter import T2IAdapterField, T2IAdapterInvocation
from invokeai.app.invocations.z_image_denoise import ZImageDenoiseInvocation
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
from invokeai.backend.stable_diffusion.schedulers.schedulers import SCHEDULER_NAME_VALUES
Expand Down Expand Up @@ -729,6 +730,52 @@ def _loras_to_json(obj: Union[Any, list[Any]]):
return LatentsMetaOutput(**params, metadata=MetadataField.model_validate(md))


@invocation(
"z_image_denoise_meta",
title=f"{ZImageDenoiseInvocation.UIConfig.title} + Metadata",
tags=["z-image", "latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
category="latents",
version="1.0.0",
)
class ZImageDenoiseMetaInvocation(ZImageDenoiseInvocation, WithMetadata):
"""Run denoising process with a Z-Image transformer model + metadata."""

def invoke(self, context: InvocationContext) -> LatentsMetaOutput:
def _loras_to_json(obj: Union[Any, list[Any]]):
if not isinstance(obj, list):
obj = [obj]

output: list[dict[str, Any]] = []
for item in obj:
output.append(
LoRAMetadataField(
model=item.lora,
weight=item.weight,
).model_dump(exclude_none=True, exclude={"id", "type", "is_intermediate", "use_cache"})
)
return output

obj = super().invoke(context)

md: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
md.update({"width": obj.width})
md.update({"height": obj.height})
md.update({"steps": self.steps})
md.update({"guidance": self.guidance_scale})
md.update({"denoising_start": self.denoising_start})
md.update({"denoising_end": self.denoising_end})
md.update({"scheduler": self.scheduler})
md.update({"model": self.transformer.transformer})
md.update({"seed": self.seed})
if len(self.transformer.loras) > 0:
md.update({"loras": _loras_to_json(self.transformer.loras)})

params = obj.__dict__.copy()
del params["type"]

return LatentsMetaOutput(**params, metadata=MetadataField.model_validate(md))


@invocation(
"metadata_to_vae",
title="Metadata To VAE",
Expand Down
10 changes: 9 additions & 1 deletion invokeai/frontend/web/src/features/metadata/parsing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,8 @@ const Qwen3EncoderModel: SingleMetadataHandler<ModelIdentifierField> = {
return Promise.resolve(parsed);
},
recall: (value, store) => {
// Clear conflicting Qwen3Source when setting Encoder (mutually exclusive)
store.dispatch(zImageQwen3SourceModelSelected(null));
store.dispatch(zImageQwen3EncoderModelSelected(value));
},
i18nKey: 'metadata.qwen3Encoder',
Expand All @@ -749,6 +751,8 @@ const ZImageVAEModel: SingleMetadataHandler<ModelIdentifierField> = {
return Promise.resolve(parsed);
},
recall: (value, store) => {
// Clear conflicting Qwen3Source when setting VAE (mutually exclusive)
store.dispatch(zImageQwen3SourceModelSelected(null));
store.dispatch(zImageVaeModelSelected(value));
},
i18nKey: 'metadata.vae',
Expand All @@ -773,6 +777,9 @@ const ZImageQwen3SourceModel: SingleMetadataHandler<ModelIdentifierField> = {
return Promise.resolve(parsed);
},
recall: (value, store) => {
// Clear conflicting VAE and Encoder when setting Qwen3Source (mutually exclusive)
store.dispatch(zImageVaeModelSelected(null));
store.dispatch(zImageQwen3EncoderModelSelected(null));
store.dispatch(zImageQwen3SourceModelSelected(value));
},
i18nKey: 'metadata.qwen3Source',
Expand Down Expand Up @@ -1000,7 +1007,6 @@ export const ImageMetadataHandlers = {
CFGRescaleMultiplier,
CLIPSkip,
Guidance,
Scheduler,
Width,
Height,
Seed,
Expand All @@ -1016,6 +1022,8 @@ export const ImageMetadataHandlers = {
RefinerNegativeAestheticScore,
RefinerDenoisingStart,
MainModel,
// Scheduler must be after MainModel so that base-dependent logic (z-image scheduler) works correctly
Scheduler,
VAEModel,
Qwen3EncoderModel,
ZImageVAEModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,19 @@ import type { MainModelConfig, Qwen3EncoderModelConfig, VAEModelConfig } from 's

/**
* Z-Image VAE Model Select - uses FLUX VAE models
* Disabled when Qwen3 Source is selected (mutually exclusive)
* Selecting this will clear Qwen3 Source (mutually exclusive)
*/
const ParamZImageVaeModelSelect = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const zImageVaeModel = useAppSelector(selectZImageVaeModel);
const zImageQwen3SourceModel = useAppSelector(selectZImageQwen3SourceModel);
const [modelConfigs, { isLoading }] = useFluxVAEModels();

// Disable when Qwen3 Source is selected
const isDisabled = zImageQwen3SourceModel !== null;

const _onChange = useCallback(
(model: VAEModelConfig | null) => {
if (model) {
// Clear conflicting Qwen3Source when setting VAE
dispatch(zImageQwen3SourceModelSelected(null));
dispatch(zImageVaeModelSelected(zModelIdentifierField.parse(model)));
} else {
dispatch(zImageVaeModelSelected(null));
Expand All @@ -48,15 +46,14 @@ const ParamZImageVaeModelSelect = memo(() => {
});

return (
<FormControl minW={0} flexGrow={1} gap={2} isDisabled={isDisabled}>
<FormControl minW={0} flexGrow={1} gap={2}>
<FormLabel m={0}>{t('modelManager.zImageVae')}</FormLabel>
<Combobox
value={value}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
isClearable
isDisabled={isDisabled}
placeholder={t('modelManager.zImageVaePlaceholder')}
/>
</FormControl>
Expand All @@ -67,21 +64,19 @@ ParamZImageVaeModelSelect.displayName = 'ParamZImageVaeModelSelect';

/**
* Z-Image Qwen3 Encoder Model Select
* Disabled when Qwen3 Source is selected (mutually exclusive)
* Selecting this will clear Qwen3 Source (mutually exclusive)
*/
const ParamZImageQwen3EncoderModelSelect = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const zImageQwen3EncoderModel = useAppSelector(selectZImageQwen3EncoderModel);
const zImageQwen3SourceModel = useAppSelector(selectZImageQwen3SourceModel);
const [modelConfigs, { isLoading }] = useQwen3EncoderModels();

// Disable when Qwen3 Source is selected
const isDisabled = zImageQwen3SourceModel !== null;

const _onChange = useCallback(
(model: Qwen3EncoderModelConfig | null) => {
if (model) {
// Clear conflicting Qwen3Source when setting Encoder
dispatch(zImageQwen3SourceModelSelected(null));
dispatch(zImageQwen3EncoderModelSelected(zModelIdentifierField.parse(model)));
} else {
dispatch(zImageQwen3EncoderModelSelected(null));
Expand All @@ -98,15 +93,14 @@ const ParamZImageQwen3EncoderModelSelect = memo(() => {
});

return (
<FormControl minW={0} flexGrow={1} gap={2} isDisabled={isDisabled}>
<FormControl minW={0} flexGrow={1} gap={2}>
<FormLabel m={0}>{t('modelManager.zImageQwen3Encoder')}</FormLabel>
<Combobox
value={value}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
isClearable
isDisabled={isDisabled}
placeholder={t('modelManager.zImageQwen3EncoderPlaceholder')}
/>
</FormControl>
Expand All @@ -117,22 +111,20 @@ ParamZImageQwen3EncoderModelSelect.displayName = 'ParamZImageQwen3EncoderModelSe

/**
* Z-Image Qwen3 Source Model Select - Diffusers Z-Image models for fallback
* Disabled when VAE or Qwen3 Encoder is selected (mutually exclusive)
* Selecting this will clear VAE and Qwen3 Encoder (mutually exclusive)
*/
const ParamZImageQwen3SourceModelSelect = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const zImageQwen3SourceModel = useAppSelector(selectZImageQwen3SourceModel);
const zImageVaeModel = useAppSelector(selectZImageVaeModel);
const zImageQwen3EncoderModel = useAppSelector(selectZImageQwen3EncoderModel);
const [modelConfigs, { isLoading }] = useZImageDiffusersModels();

// Disable when VAE or Qwen3 Encoder is selected
const isDisabled = zImageVaeModel !== null || zImageQwen3EncoderModel !== null;

const _onChange = useCallback(
(model: MainModelConfig | null) => {
if (model) {
// Clear conflicting VAE and Encoder when setting Qwen3Source
dispatch(zImageVaeModelSelected(null));
dispatch(zImageQwen3EncoderModelSelected(null));
dispatch(zImageQwen3SourceModelSelected(zModelIdentifierField.parse(model)));
} else {
dispatch(zImageQwen3SourceModelSelected(null));
Expand All @@ -149,15 +141,14 @@ const ParamZImageQwen3SourceModelSelect = memo(() => {
});

return (
<FormControl minW={0} flexGrow={1} gap={2} isDisabled={isDisabled}>
<FormControl minW={0} flexGrow={1} gap={2}>
<FormLabel m={0}>{t('modelManager.zImageQwen3Source')}</FormLabel>
<Combobox
value={value}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
isClearable
isDisabled={isDisabled}
placeholder={t('modelManager.zImageQwen3SourcePlaceholder')}
/>
</FormControl>
Expand Down
Loading